欢迎光临
我们一直在努力

北漂码农的我,把在大城市过成了屯子一样舒服,哈哈哈哈哈!

lei阅读(2)


作者:小傅哥
博客:https://bugstack.cn
Github:https://github.com/fuzhengwei/CodeGuide/wiki

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

东北老家,很久没回去了!

可能是写代码改变生活吧!😄

一家人从东北来到京津冀后,我自己基本很久没回去过了。上段时间小学同学路过我家门口,拍了张照片发给我,看后确实很怀念,嗯!很怀念!后面放假了,再回去转一转!不吹牛的讲,东北的烤串、麻辣烫最香!

接下来讲讲关于我是自己北漂的故事,是怎么一步步把大城市的生活,过成屯子!

二、🚇地铁坐不动了!

🚇地铁坐不动了!

在北京这个大城市,如果能过的像在县城里那样,还真的是挺舒服的!

我可能本身不是一个特别喜欢大城市的人,尤其市中心那种上下班时:道路的嘈杂人流的拥挤还有通勤路途的遥远。有时候看似不太远的路,40公里、50公里,但放在我的老家可能就已经出门跑到榆树或者舒兰了!

刚毕业上班时,我陆续租住过:立水桥房山长阳大瓦窑,找房子的核心目标就是便宜,哪里便宜就住哪里。最远的时候,单程上班差不多要2个小时(好在那时候6点就下班了),早年手机也没那么多流量,只能下载个电影充饥无聊!不敢快进,怕路上不够看!

但从15年进入互联网企业后,就不行了,加班也多、下班也晚了、🛌睡眠也不够。就很难抗住每天早上6点起来坐地铁去,所以随着公司搬家,立马就搬家到公司身边了。从此过上没羞没臊的日子,18分钟走路上班!

三、🚶走路上班很幸福!

🚶走路上班很幸福!

自从可以走路上班后,我膨胀了还买了一个电动车🛵,骑车上班3~5分钟就到了。周边的城乡世纪广场、南海子公园、马驹桥等等,几乎都可以抵达,但有时候回来也就没电了。

更主要的是通勤时间短、通勤容易以后,就有了很多自己的时间。早上起床洗漱完也就7点多,再看看书、写写东西,或者刷一会视频还是蛮幸福的。有时候即使晚上加班9点、10点回来,也不会觉得特别累,如果回来的再早点基本就又有自己的时间可以搞点东西了。

为了能把这份快乐延续下去,我还把第一个窝安在了廊坊(北京买不起),距离上班总部40公里。平时如果不加班也可以坐班车回家,还可以选择坐公交车,如果打车的话差不多50分钟可以到家。哎、可惜没有京牌,也没有在早期选择一个电动车牌。所以有的时候选择还是很重要的!

不想生活只是活着,就还是需要奔着有目标的方向选择!

四、🏃北漂落户天津!

这可能是很多留不下北京,又回不去家乡的东北人,最佳选择吧。

刚毕业时还真有机会拿北京户口,但被我放弃了!哈哈哈哈哈,我选择了多要几百块钱住宿补助!

从决定落户到拿到户口本,用了将近3年时间。从17年找中介办理集体户口,准迁证、调档、迁入、户籍开通、拿集体户口个人页、办理新身份证、买房、等待交房、开不动产证明、办理个人户籍,终于在2020年,拿到了这个小红本!

天津小红本

虽然,落户了天津,但还不知道什么时候才会去天津!其实还是挺期待以后可以安静的在天津生活,做一些自己想做的事情。比如,开个码农会所,哈哈哈哈哈!

五、🤔认知范围决定生活!

  • 如果我数学不好,没选择软件工程
  • 如果我毕业就业,没选择北京北漂
  • 如果我第一份工作,就想好了要拿北京户口
  • 如果我没选择从传统行业跳槽到互联网
  • 如果我放弃油车摇号,排队电动车
  • 等等…

你现在的生活,基本是由你的认知范围决定,你的认知范围又是由你的知识储备支撑的。

这就像我们夜晚都开车在高速上,虽然路一样宽,但你的灯不那么亮,他的灯亮。那么他看到的就多、看到的就远,他也就有更多的时间提前做出反应。而你是不可能已经错过了下高速的路口!

有句话说(13年·我说的),人生其实没有选择,因为有些选项只是摆设!

人生其实没有选择,因为有些选项只是摆设!

之所以哪些看似更好的选择是摆设,是因为我们知识储备不足,所以视觉盲区就会很大。这就像你好似很费力的给人家讲一个道理,但换来的是喋喋不休的争吵。就像蚂蚱问孔子一年有三季,孔子说:三季一样。没有共同的认知,就没有必要争吵。

沉淀、积累、破局,几乎是我们普通人突破赛道的唯一途径!

😄好了,本期就扯到这咯!可能你我都有类似的人生经历,如果能给你一些借鉴,感谢点个赞、留个言,这样指不定某天我们就在天津碰面了!

六、系列推荐

https://segmentfault.com/a/1190000038324684

cheat.sh在手,天下我有

lei阅读(3)

前言

作为程序员需要了解的东西有很多,日常编码和写脚本脱离不开各式语言与 Linux 命令。为了记住一些杂乱的或不被经常使用的知识点,我们迫切需要一个“小抄”/备忘录,小抄内容多了自然繁杂,所以我们希望这个小抄要:

  1. 简洁:只包含你想要的内容,没有其他「花边」内容
  2. 快速:可以立即使用
  3. 全面:能基本包含你所有问题的答案
  4. 通用:它应该在任何地方、任何时间都可用,不需要任何准备
  5. 不唐突:它不应该让你从主要任务上分心(比如减少应用切换)
  6. 辅导:它应该帮助你学习这个科目(在答案基础上扩展知识)
  7. 不显眼:应该可以在完全不被注意的情况下使用(就好比划词翻译,鼠标轻点就有答案)

老gong,你是想介绍哆啦A梦吗?

<img src=”https://cdn.jsdelivr.net/gh/FraserYu/img-host/blog-img20201115205225.png” style=”zoom:25%;” />

非也,其实是 cheat.sh

cheat.sh 介绍

cheat.sh 就是一个可以满足上述愿望的小哆啦,目前在 Github 的形式是这样滴:

Commit 也非常活跃,就是这么一个哆啦

  • 它提供一个简单的 curl/浏览器接口方便我们查询
  • 目前覆盖 58 种编程语言,多种 DBMS以及超过 1000 个UNIX/Linux 常用命令
  • 提供对世界上最好的社区驱动的备忘单存储库的访问,与StackOverflow持平(绝对是高质量的内容)
  • 提供命令行客户端
  • 可以嵌套在代码编辑器中使用,比如 Intellij IDEA 和 VS-Code
  • 支持一个特殊的隐身模式,可以完全隐形的使用它 (感觉挺神秘的呢)

先来认识一下,打开命令行终端,使用 curl 命令输入:

curl cht.sh

如何使用 cheat.sh

先拿几个常用的 UNIX/Linux 命令练练手:

curl cht.sh/tar

瞧这整理的规范和简洁不?

curl cht.sh/tr

答案依旧整洁规范, 同时还高亮显示,友好的很啊

如果你不知道某个命令,还可以使用 ~Keyword 的形式来查询,比如你想查看如何建立快照

curl cht.sh/~snapshot

上面说过, cheat.sh 包含 1000 多个常用的 UNIX/Linux 命令,当需要的时候,按照语法 curl cht.sh/<you-cmd> 尽情查询吧

除了 Linux 命令,我们还说支持 58 种语言,当写代码时某个 API 不会用或需要完成某些操作,cheat.sh 依旧可以帮上忙,比如我总是记不住 Java Lambda 的 group 操作

curl cht.sh/java/lambda+group

记住下面的标准格式,搜索的结果都是和 StackOverflow 一样的高质量

如果这个答案还不是你想要的,你就可以添加数字进行翻页获取其他结果

另外你觉得结果中的注释很碍眼的话,可以在每次查询的后面加上 /?Q ,就像这样:

curl cht.sh/java/lambda+group/?Q

当然每种语言都默认支持 :list 和 :help 两种查询方式,作为帮助指令,大家可以自行尝试了,比如 go 语言

curl cht.sh/go/:list
curl cht.sh/go/:help

相信到这里,你已经可以掌握 cheat.sh 的基本使用方式了

但是,这种 curl 方式总是显得不是很方便,比如空格要用 + 替代,日常工作语言比如只有 Java,每次都要输入 curl cht.sh/java/xxxxxxx 这样就会显得很麻烦, 为了解决这些问题,cheat.sh 很贴心,也提供了命令行客户端,大大简化了搜索操作

Cheat.sh 命令行客户端

安装

安装 CLI Client (Command Line Client)非常简单,只需要依次执行下面的命令即可:

# 注意你的环境变量 PATH 已经 import 了 ~/bin 下的内容
mkdir -p ~/bin/
curl https://cht.sh/:cht.sh > ~/bin/cht.sh
chmod +x ~/bin/cht.sh

如果要保证 shell 模式可用,还需要安装一个依赖 rlwrap, 下面两种安装方式都可以(我直接用brew安装的)

brew install rlwrap
# 或者
sudo apt install rlwrap

使用

有了 CLI Client 之后,来看一看搜索上的变化:

这个 CLI Client 还提供了一个更加便利的 shell 模式:

cht.sh --shell

如下图,每次直接按照语言搜索相关内容就可以了:

通常我们编程在一段时间内会用一种语言,我们可以进一步简化搜索过程,cd 到某个语言目录下:

如果进入 shell 模式,同时想一次性进入某个语言目录,也可以通过一条命令搞定:

cht.sh --shell java

隐身模式

相信很多小伙伴都配有划词工具,比如某个单词不会了,选中相应的单词,就会出来解释,cheat.sh 也有类似的模式,进入某个语言目录下之后,输入 stealth Q 就可以进入这个模式了:

用鼠标选中文本后,用起来的效果就是这样滴:

不过这里建议,搜索的单词不要超过 5 个

以上这些使用方式,默认都会调用它自己的服务,为了更快速的响应,我们可以搭建自己的服务,前提是要更改 CLI Client 的 server URL:

打开或新建 ~/.cht.sh/cht.sh.conf ,添加

CHTSH_URL=https://cht.sh            # URL of the cheat.sh server

然后就可以 run 自己的服务

git clone https://github.com/chubin/cheat.sh.git
cd cheat.sh
docker-compose up

最后访问服务: http://localhost:8002

集成主流编辑器

cheat.sh 同样和主流编辑器有很好的集成:

Feature Emacs Sublime Vim VSCode IDEA QtCreator
Command queries
Queries from buffer
Toggle comments
Prev/next answer
Multiple answers
Warnings as queries
Queries history
Session id
Configurable server

Vim 的集成度是最高的,大家可以根据 cheat.sh-vim 自行配置

VSCode 和 IDEA 是大家高频使用的两个 IDE,和他们集成就很简单了,只需要安装相应的插件:

VSCode 插件

安装 vscode-snippet 就可以在 VSCode 中快速使用这个功能了

IDEA 插件

安装 idea-cheatsh-plugin 这个插件就可以在 Intellij IDEA 中使用这个功能了

总结

至于支持的 58 种语言都是什么,请大家自行参考 README 文档,关于 cheat.sh, 了解这些基本的使用就已经够了,还是那句话,好的工具是用来提高工作效率的,不要被工具过度捆绑
日拱一兵 | 原创

https://segmentfault.com/a/1190000038323985

NodeJS: 从 0 开始 Prometheus + Grafana 业务性能指标监控

lei阅读(3)

为什么需要指标监控告警

一个复杂的应用,往往由很多个模块组成,而且往往会存在各种各样奇奇怪怪的使用场景,谁也不能保证自己维护的服务永远不会出问题,等用户投诉才发现问题再去处理问题就为时已晚,损失已无法挽回。

所以,通过数据指标来衡量一个服务的稳定性和处理效率,是否正常运作,监控指标曲线的状态,指标出现异常时及时主动告警,这一套工具就十分重要。

常见的一些指标,包括但不限于:

  • QPS
  • 请求处理耗时
  • 进程占用内存
  • 进程占用CPU
  • golang 服务的 goroutine
  • nodejs 的 event loop lag
  • 前端应用的 Performance 耗时

举个例子,假如一个服务:

  • 使用内存随着时间逐渐上涨
  • CPU 占用越来越高
  • 请求耗时越来越高,请求成功率下降
  • 磁盘空间频频被挤爆

<!–这到底是人性的扭曲还是道德的沦丧,–>
一旦服务存在某些缺陷导致这些问题,通过服务日志,很难直观快速地察觉到这些指标的变化波动。

通过监控和告警手段可以有效地覆盖了「发现」和「定位」问题,从而更有效率地排查和解决问题。

指标监控系统:Prometheus

Prometheus 是一个开源的服务监控系统和时间序列数据库。

工作流可以简化为:

  1. client 采集当前 机器/服务/进程 的状态等相关指标数据
  2. Prometheus server 按一定的时间周期主动拉取 client 的指标数据,并存储到时序数据库中
  3. 发现指标异常后,通过 alert manager 将告警通知给相关负责人

具体的架构设计如下:

为什么不用 mysql 存储?

Prometheus 用的是自己设计的时序数据库(TSDB),那么为什么不用我们更加熟悉,更加常用的 mysql, 或者其他关系型数据库呢?

假设需要监控 WebServerA 每个API的请求量为例,需要监控的维度包括:服务名(job)、实例IP(instance)、API名(handler)、方法(method)、返回码(code)、请求量(value)。

image

如果以SQL为例,演示常见的查询操作:

# 查询 method=put 且 code=200 的请求量
SELECT * from http_requests_total WHERE code=”200” AND method=”put” AND created_at BETWEEN 1495435700 AND 1495435710;

# 查询 handler=prometheus 且 method=post 的请求量
SELECT * from http_requests_total WHERE handler=”prometheus” AND method=”post” AND created_at BETWEEN 1495435700 AND 1495435710;


# 查询 instance=10.59.8.110 且 handler 以 query 开头 的请求量
SELECT * from http_requests_total WHERE handler=”query” AND instance=”10.59.8.110” AND created_at BETWEEN 1495435700 AND 1495435710;

通过以上示例可以看出,在常用查询和统计方面,日常监控多用于根据监控的维度进行查询与时间进行组合查询。如果监控100个服务,平均每个服务部署10个实例,每个服务有20个API,4个方法,30秒收集一次数据,保留60天。那么总数据条数为:100(服务)* 10(实例)* 20(API)* 4(方法)* 86400(1天秒数)* 60(天) / 30(秒)= 138.24 亿条数据,写入、存储、查询如此量级的数据是不可能在Mysql类的关系数据库上完成的。 因此 Prometheus 使用 TSDB 作为 存储引擎。

时序数据库(Time Series Database/TSDB)

时序数据库主要用于指处理带时间标签(按照时间的顺序变化,即时间序列化)的数据,带时间标签的数据也称为时序数据。

对于 prometheus 来说,每个时序点结构如下:

  • metric: 指标名,当前数据的标识,有些系统中也称为name。
  • label: 标签属性
  • timestamp: 数据点的时间,表示数据发生的时间。
  • value: 值,数据的数值

每个指标,有多个时序图;多个时序数据点连接起来,构成一个时序图

image

假如用传统的关系型数据库来表示时序数据,就是以下结构:

create_time __metric_name__ path value
2020-10-01 00:00:00 http_request_total /home 100
2020-10-01 00:00:00 http_request_total /error 0
2020-10-01 00:00:15 http_request_total /home 120
2020-10-01 00:01:00 http_request_total /home 160
2020-10-01 00:01:00 http_request_total /error 1

指标 request_total{path=”/home”} 在 2020-10-01 00:01:00 时的 qps = (160 – 100)/60 = 1 , 同理,
指标 request_total{path=”/error”} 在 2020-10-01 00:01:00 时的 qps = 1/60

相比于 MySQL,时序数据库核心在于时序,其查询时间相关的数据消耗的资源相对较低,效率相对较高,而恰好指标监控数据带有明显的时序特性,所以采用时序数据库作为存储层

数据类型

  • counter: 计数器,只能线性增加,不断变大,场景:qps
  • gauge:绝对值,非线性,值可大可小,场景:机器温度变化,磁盘容量,CPU 使用率,
  • histogram:,聚合数据查询耗时分布【服务端计算,模糊,不精确】
  • summary:不能聚合查询的耗时分布【客户端计算,精确】

nodejs 指标采集与数据拉取

  • 定义一个 Counter 的数据类型,记录指标
const reqCounter = new Counter({
  name: `credit_insight_spl_id_all_pv`,
  help: 'request count',
  labelNames: ['deviceBrand','systemType', 'appVersion', 'channel']
})

reqCounter.inc({
  deviceBrand: 'Apple',
  systemType: 'iOS',
  appVersion: '26014',
  channel: 'mepage'
},1)
  • 定义访问路径为 /metrics 的controller
  @Get('metrics')
  getMetrics(@Res() res) {
    res.set('Content-Type', register.contentType)
    res.send(register.metrics())
  }
  • Prometheus 主动请求 node client 的 /metrics 接口,获得

当前数据快照

image

promQL

promQL 是 prometheus 的查询语言,语法十分简单

基本查询

查询指标最新的值:

{__name__="http_request_total", handler="/home"}

# 语法糖:
http_request_total{handler="/home"}

# 等价于 mysql:
select * from http_request_total 
where 
  handler="/home" AND
  create_time=《now()》

区间时间段查询

查询过去一分钟内的数据

# promQL
http_request_total[1m]

# 等价于
SELECT * from http_requests_total 
WHERE create_time BETWEEN 《now() - 1min》 AND 《now()》;

时间偏移查询

PS: promQL 不支持指定时间点进行查询,只能通过 offset 来查询历史某个点的数据

查询一个小时前的数据。

# promQL
http_request_total offset 1h

# 等价于
SELECT * from http_requests_total 
WHERE create_time=《now() - 1 hour》;

promQL 查询函数

根据以上的查询语法,我们可以简单组合出一些指标数据:

例如,查询最近一天内的 /home 页请求数

http_request_total{handler="/home"}  - http_request_total{handler="/home"} offset 1d

那么实际上面这个写法很明显比较不简洁,我们可使用内置 increase 函数来替换:

# 和上述写法等价
increase(http_request_total{handler="/home"}[1d])

除了 increase 外,还有很多其他好用的函数,例如,
rate 函数计算 QPS

// 过去的 2 分钟内平均每秒请求数
rate(http_request_total{code="400"}[2m])

// 等价于
increase(http_request_total{code="400"}[2m]) / 120

指标聚合查询

除了上述基础查询外,我们可能还需要聚合查询

假如我们有以下数据指标:

credit_insight_spl_id_all_pv{url="/home",channel="none"} 
credit_insight_spl_id_all_pv{url="/home",channel="mepage"} 
credit_insight_spl_id_all_pv{url="/error",channel="none"} 
credit_insight_spl_id_all_pv{url="/error",channel="mepage"} 

将所有指标数据以某个维度进行聚合查询时,例如:查询 url=”/home” 最近一天的访问量,channel 是 none还是mepage 的 /home 访问量都包括在内。

我们理所当然地会写出:

increase(credit_insight_spl_id_all_pv{url="/home"}[1d])

但实际上我们会得出这样的两条指标结果:

credit_insight_spl_id_all_pv{url="/home",channel="none"} 233
credit_insight_spl_id_all_pv{url="/home",channel="mepage"} 666

image

并非我们预期中的:

credit_insight_spl_id_all_pv{url="/home"} 899

而要是我们想要得到这样的聚合查询结果,就需要用到 sum by

# 聚合 url="/home" 的数据
sum(increase(credit_insight_spl_id_all_pv{url="/home"}[1d])) by (url)
# 得出结果:
credit_insight_spl_id_all_pv{url="/home"} 899    # 所有 channel 中 /home 页访问量累加值


# 聚合所有的 url 则可以这样写:
sum(increase(credit_insight_spl_id_all_pv{}[1d])) by (url)
# 得出结果:
credit_insight_spl_id_all_pv{url="/home"} 899  
credit_insight_spl_id_all_pv{url="/error"} 7


# 等价于 mysql
SELECT url, COUNT(*) AS total FROM credit_insight_spl_id_all_pv 
WHERE create_time between <now() - 1d> and <now()>
GROUP BY url; 

指标时序曲线

以上的所有例子的查询数值,其实都是最近时间点的数值,

而我们更关注的是一个时间段的数值变化。

要实现这个原理也很简单,只需要在历史的每个时间点都执行一次指标查询,

# 假如今天7号
# 6号到7号的一天访问量
sum(increase(credit_insight_spl_id_all_pv{}[1d] )) by (url) 

# 5号到6号的一天访问量 offset 1d 
sum(increase(credit_insight_spl_id_all_pv{}[1d] offset 1d)) by (url) 

# 4号到5号的一天访问量
sum(increase(credit_insight_spl_id_all_pv{}[1d] offset 2d)) by (url) 

而 Prometheus 已经内置了时间段查询功能,并对此优化处理。

可通过 /api/v1/query_range 接口进行查询,获的 grpah:
image

Prometheus 查询瓶颈

数据存储:

指标数据有 “Writes are vertical,reads are horizontal” 的(垂直写,水平读)模式:
“Writes are vertical,reads are horizontal” 的意思是 tsdb 通常按固定的时间间隔收集指标并写入,会 “垂直” 地写入最近所有时间序列的数据,而读取操作往往面向一定时间范围的一个或多个时间序列,“横向” 地跨越时间进行查询

  • 每个指标(metric)根据指标数量不同,有 labelA labelB labelC * … 个时序图
  • 每个时序图(time series)的一个点时序是 [timestamp, value], 例如 [1605607257, 233]。[时间戳-值] 可以确定图上的一个点,一个时间区间内的所有点连成一个时序曲线图。
  • 因为 Prometheus 每隔 15s 采集一次数据,所以 时序点的时间间距是 15s,即1分钟有60/15=4个时序点,1小时就有 4 * 60 = 240 个时序点。

image

而 Prometheus 的默认查询 sample 上限是 5000w

image

所以,如果指标的时序图数量过大,允许查询的时间区间相对就会较小了

一个图表查询时序数量的影响因素有 3 个,分别是:

  1. 查询条件的时序数量(n)
  2. 查询的时间区间(time)
  3. 图表曲线每个时序点之间的间隔(step)

credit_insight_spl_id_all_pv 指标为例,该指标总共大约有 n = 163698 种时序,
image

假如 step = 15s,如果搜索该指标过去 time = 60m 的全部时序图,那么,需要搜索的例子要
163698 * 60 * (60/15) = 39287520,将近 4kw,是可以搜出来的。

但如果搜的是过去 90m 的数据,163698 * 90 * 4 = 58931280,超过了 5000w,你就发现数据请求异常:
Error executing query: query processing would load too many samples into memory in query execution
image

所以,目测可得一个图的查询时序点数量公式是:total = n * time / step, time 和 step 的时间单位必须一致,total 必须不超过 5000w。

反推一下得出,time < 5000w / n * step 。要扩大搜索时间范围,增大 step ,或者降低 n 即可做到。

  • step 不变, 降低 n 【指定label值可减少搜索条件的结果数】 : credit_insight_spl_id_all_pv{systemType="Android", systemVersion="10"},n = 18955

image

  • 增大 step 到 30s, n 不变:

image

当然,一般情况下,我们的 n 值只有几百,而 step 基本是大于 60s 的,所以一般情况下都能查询 2 个多月以上的数据图。

可视化平台: Grafana

grafana 是一个开源的,高度可配置的数据图表分析,监控,告警的平台,也是一款前端可视化的产品。

image

自定义图表

grafana 内置提供多种图表模板,具体是以下类型:

Prometheus 作为数据源的情况下,一般用的 graph 类型画时序图比较多。

对于一些基础的数据大盘监控,这些图表类型已经足够满足我们的需求。

但对于复杂的需求,这些类型无法满足我们的需要时,我们安装 pannel 插件,来更新可用的图表类型,也可以根据官方文档 build a panel plugin 开发自己的前端图表 panel。

图表配置

在时序图表配置场景下,我们需要核心关注配置的有:

  1. promQL: 查询语句
  2. Legend: 格式化图例文本
  3. step/interval: 采集点间隔,每隔一段时间,采集一次数据。
    一条曲线的数据点数量 = 图表时长 / 采样间隔。例如查看最近24小时的数据,采样 间隔5min,数据点数量=24*60/5=288。
    采集间隔时间越短,采样率越大,图表数据量越大,曲线越平滑。 采集间隔默认自动计算生成,也可以自定义配置。
  4. metric time range: 每个点的数据统计时间区间时长。
    以QPS为例,图表上每个时间点的数据的意义是:在这时间点上,过去n秒间的访问量。

从上图可以看到,

  • 如果采样间隔 > 统计区间时长: 数据采样率 < 100%。未能采集到的数据丢弃,不会再图表上展示。采样率过小可能会错误异常的数据指标。
  • 如果采样间隔 == 统计区间时长,采样率100%。
  • 如果采样间隔 < 统计区间时长,数据被重复统计,意义不大。

自定义变量

为了实现一些常用的筛选过滤场景,grafana 提供了变量功能

  • 变量配置:变量配置有多种方式(Type),可以自定义选项,也可以根据prometheus 指标的 label 动态拉取。

image

  • 变量使用:变量通过 $xxx 形式去引用。

image

告警

除了 Prometheus 本身可以配置告警表达式之外:

image

grafana 也可以配置告警:

数据源

Prometheus 通常用于后端应用的指标数据实时上报,主要用于异常告警,问题排查,所以数据存在时效性,我们不会关注几个月前的一个已经被排查并 fixed 的指标异常波动告警。

但是,要是我们将 Prometheus 用于业务指标监控,那么我们可能会关注更久远的数据。

例如我们可能想要看过去一个季度的环比同比增长,用 Prometheus 作为数据源就不合适,因为 Prometheus 是时序数据库,更多关注实时数据,数据量大,当前数据保存的时效设定只有 3 个月。

那么这个时候可能我们要维护一个长期的统计数据,可能就需要存储在 mysql 或者其他存储方式。

grafana 不是 Prometheus 的专属产品,还支持多种数据源,包括但不限于:

  • 常见数据库

    • MySql
    • SQL Server
    • PostgreSQL
    • Oracle
  • 日志、文档数据库

    • Loki
    • Elasticsearch
  • 时序数据库

    • Prometheus
    • graphite
    • openTSDB
    • InfluxDB
  • 链路追踪

    • Jaeger
    • Zipkin
  • ….

如果没有自己需要的数据源配置,还可以安装 REST API Datasource Plugin, 通过 http 接口查询作为数据源

总结

了解 grafana 的高度可配置性设计后,有值得思考的几点:

  • 关注其设计思想,如果要自己实现一个类似的可视化的 web app,自己会怎么设计?
  • 自己要做一个高度可配置化的功能,又应该怎么设计?
  • 深入到业务,例如我们常用的 admin 管理 系统,一些常用的业务功能是否可以高度可配置化?业务强关联的如何做到配置与业务的有机结合?

等等这些,其实都是值得我们去思考的。

此外,Prometheus 和 grafana 都有些进阶的玩法,大家有兴趣也可以去探索下。

参考文章

  1. Prometheus 的数据存储实现【理论篇】
  2. prometheus tsdb 的存储与索引
  3. query processing would load too many samples into memory in query execution

https://segmentfault.com/a/1190000038321386

南京有哪些“牛批”的互联网公司?

lei阅读(2)

小伙伴们周末快乐!

之前咱们这里发过一篇文章,聊了聊深圳有哪些优秀的互联网公司

聊到深圳的互联网公司,咱还敢正儿八经地用“牛批”这一词,但是聊到蓝鲸,说互联网公司,还敢用“牛批”这个词是有点慌的,有点怕被打哇。为啥呢?

一、二线城市在IT技术氛围上的一个比较明显的区别那就是:一线大城市主要是那些原生本土的IT企业、互联网公司比较多,所以技术氛围会很好,IT文化也更加繁荣,跳槽时候可选的机会的确要多不少。因此像北上深这类城市,基本上知名的互联网/通信/软件公司的本部都在那儿,而且这些大公司衍生出来的一系列生态链公司和下游公司也会多很多。

但是二线城市则不然,本土所萌芽出来的IT/互联网/通信/软件等公司还是少了一点,大部分情况都是大公司所设置的研发中心(或分部),所以技术氛围肯定没有一线城市那么繁荣。而且二线城市还有一个特点就是外包公司特别多,尤其是软件外包公司,像我们这边甚至有那种专门的软件外包园。

所以说回本文的话题,今天我们来聊一聊六朝古都南京有哪些IT/技术类公司(也不仅仅局限于纯互联网公司哈,有些公司也有互联网业务)。

注意,我们只列举,不做过多评论,且排名不分先后,有些可能不一定总结得全,小伙伴们有补充的可以留言,众人拾柴火焰高。

上菜!


华为南研所

中兴南研所

  • 类型:通信设备商
  • 地址:南京市雨花台区中兴通讯南研1区、2区
  • 网址:http://job.zte.com.cn/
    • *

阿里巴巴南京

小米南京

  • 类型:移动互联网
  • 地址:南京市建邺区新城科技园小米研发中心
  • 网址:https://hr.xiaomi.com/
    • *

苏宁系

苏宁易购、苏宁金融、苏宁体育等等。

  • 类型:电商
  • 地址:南京市玄武区徐庄软件园苏宁总部
  • 网址: www.suning.cn
    • *

vivo南京

  • 类型:通信、移动互联网
  • 地址:南京市雨花台区南京雨花台区vivo大厦
  • 网址:http://hr.vivo.com.cn/
    • *

烽火通信

焦点科技

  • 类型:电商服务提供商
  • 地址:南京高新技术产业开发区星火路9号软件大厦A座
  • 网址:https://hr.focuschina.com/
    • *

帆软

途牛旅游网

  • 类型:电商、旅游
  • 地址:南京市玄武区玄武大道699-32号途牛大厦
  • 网址:http://www.tuniu.com
    • *

千米网

联创科技

  • 类型:互联网服务、软件
  • 地址:南京市鼓楼区集慧路18号联创科技大厦
  • 网址:http://www.lianchuang.com
    • *

浩鲸科技

扇贝

领添

  • 类型:外贸电商
  • 地址:南京市雨花台区软件大道170-1号天溯科技园5栋
  • 网址:http://www.sheincorp.cn/
    • *

地平线

  • 类型:人工智能、物联网
  • 地址:南京栖霞区经济开发区兴智路6号兴智科技园A栋20F
  • 网址:https://www.horizon.ai/
    • *

孩子王

三六五网络

  • 类型:互联网居家服务商
  • 地址:南京建邺区江苏省建大厦B座
  • 网址:http://www.house365.com
    • *

金智教育

  • 类型:移动互联网、教育
  • 地址:南京市江宁区中南谷创新中心
  • 网址:http://www.wisedu.com/
    • *

果酱音乐

  • 类型:文娱
  • 地址:南京雨花台区软件大道180号南京大数据产业基地7栋306室
  • 网址:https://www.jammyfm.com/join
    • *

车300

  • 类型:数据服务
  • 地址:南京市鼓楼高新区模范路科技创新街区C座南6楼
  • 网址:http://www.che300.com
    • *

美篇

  • 类型:移动互联网、社交
  • 地址:江苏省南京市雨花台区安德门大街57号楚翘城
  • 网址:https://www.meipian.cn/
    • *

厚建软件

  • 类型:软件服务商
  • 地址:南京市雨花台区安德门大街57号楚翘城3号楼
  • 网址:http://www.hoge.cn/
    • *

YOHO

  • 类型:电商
  • 地址:南京市建邺区嘉陵江东街18号南京国家广告产业园
    • *

运满满

  • 类型:物流、运输
  • 地址:江苏省南京市雨花区软件大道170-1号天溯科技园
  • 网址:http://www.ymm56.com/
    • *

中新赛克

亚信

  • 类型:移动互联网、企业服务
  • 地址:南京市雨花台区软件大道大数据产业基地
  • 网址:http://www.asiainfo.com
    • *

虹软

润和软件

  • 类型:方案提供商、平台运营商
  • 地址:南京市雨花台区软件大道168号雨花润和软件
  • 网址:http://www.hoperun.com/
    • *

趋势科技

育儿网

其他

  • 西祠胡同
  • 爱立信南京
  • 乐酷科技
  • 京东南京
  • 字节跳动南京
  • 东软南京
  • 国电南瑞
  • 三星电子(中国)研发中心
  • 快钱南京团队
  • 零号线
  • 化龙巷(常州论坛)
  • 车置宝
  • 农分期
  • 艺厘米
  • 云账房
  • …等等

最后,由于了解有限,难免会有疏漏和不当,小伙伴们可以补充哇,众人拾柴火焰高!

https://segmentfault.com/a/1190000038323111

聊聊golang的error包装

lei阅读(3)

本文主要研究一下golang的error包装

error

type error interface {
    Error() string
}

error接口定义了Error方法,返回string

runtime.Error

package runtime

type Error interface {
    error
    // and perhaps other methods
}

对于panic,产生的则是runtime.Error,该接口内嵌了error接口

wrap

package main

import (
    "errors"
    "fmt"

    pkgerr "github.com/pkg/errors"
)

func main() {
    if err := methodA(false); err != nil {
        fmt.Printf("%+v", err)
    }
    if err := methodA(true); err != nil {
        fmt.Printf("%+v", err)
    }
}

func methodA(wrap bool) error {
    if err := methodB(wrap); err != nil {
        if wrap {
            return pkgerr.Wrap(err, "methodA call methodB error")
        }
        return err
    }
    return nil
}

func methodB(wrap bool) error {
    if err := methodC(); err != nil {
        if wrap {
            return pkgerr.Wrap(err, "methodB call methodC error")
        }
        return err
    }
    return nil
}

func methodC() error {
    return errors.New("test error stack")
}

使用内置的errors,则没办法打印堆栈;使用pkg/errors可以携带堆栈

输出

test error stack
test error stack
methodB call methodC error
main.methodB
        /error-demo/error_wrap.go:33
main.methodA
        /error-demo/error_wrap.go:21
main.main
        /error-demo/error_wrap.go:15
runtime.main
        /usr/local/go/src/runtime/proc.go:204
runtime.goexit
        /usr/local/go/src/runtime/asm_amd64.s:1374
methodA call methodB error
main.methodA
        /error-demo/error_wrap.go:23
main.main
        /error-demo/error_wrap.go:15
runtime.main
        /usr/local/go/src/runtime/proc.go:204
runtime.goexit
        /usr/local/go/src/runtime/asm_amd64.s:1374% 

小结

  • error接口定义了Error方法,返回string;对于panic,产生的则是runtime.Error,该接口内嵌了error接口
  • 使用内置的errors,则没办法打印堆栈;使用pkg/errors可以携带堆栈

doc

https://segmentfault.com/a/1190000038322375

Flutter · Python AI 弹幕播放器来袭

lei阅读(3)

AI智能弹幕(也称蒙版弹幕):弹幕浮在视频的上方却永远不会挡住人物。起源于哔哩哔哩的web端黑科技,而后分别实现在IOS和Android的app端,如今被用于短视频、直播等媒体行业,用户体验提升显著。<br/>

本文除了会使用Flutter新方案进行跨端实现,同时也会讲解如何将一段任意视频流使用opencv-python处理成蒙版数据源,达成从0到1的前后端AI体系。先来看看双端最终运行效果吧:

自行clone源码打包:Zoe barrage
IPhone运行录屏:点这里
APP运行截图:

实现流程目录

  • Python后端:

    • 依次提取视频流的 关键帧 保存为图片
    • 将所有关键帧传给 神经网络模型 让算法将图片中非人物抹去,并保存图片帧
    • 将只含有人物的图片帧进行 像素色值转换,得到 灰度图,最后再转为 黑白反色图
    • 通过识别黑白反色图的 轮廓坐标 ,生成一份 时间:路径 配置文件提供给前端
  • Flutter前端:

    • 实现一个弹幕调度动画组
    • 根据 配置文件 将弹幕外层容器 裁剪 为一个刚好透出人物的漏洞形状,也称蒙版
    • 引入播放器,视频流播放时,为 关键帧 同步渲染其对应的蒙版形状
  • 拓展:

    • Web前端实现
    • 视频点播与直播
    • 总结与优化

1. Python后端

1.1 提取关键帧
# config.py  ---  配置文件
import os
import cv2

VIDEO_NAME = 'source.mp4'     # 处理的视频文件名
FACE_KEY = '*****'          # AI识别key
FACE_SECRET = '*****'       # AI密钥

dirPath = os.path.dirname(os.path.abspath(__file__))
cap = cv2.VideoCapture(os.path.join(dirPath, VIDEO_NAME))
FPS = round(cap.get(cv2.CAP_PROP_FPS), 0)

# 进行识别的关键帧,FPS每上升30,关键帧间隔+1(保证flutter在重绘蒙版时的性能的一致性)
FRAME_CD = max(1, round(FPS / 30))

if cv2.CAP_PROP_FRAME_COUNT / FRAME_CD >= 900:
    raise Warning('经计算你的视频关键帧已经超过了900,建议减少视频时长或FPS帧率!')

在这份配置文件中,会先读取视频的帧率,30FPS的视频会吧每一帧都当做关键帧进行处理,60FPS则会隔一帧处理一次,这样是为了保证Flutter在绘制蒙版的性能统一。<br/>
另外需要注意的是由于演示DEMO为完全离线环境,视频和最终蒙版文件都会被打包到APP,视频文件不宜过大。

# frame.py  ---  视频帧提取
import os
import shutil
import cv2
import config

dirPath = os.path.dirname(os.path.abspath(__file__))
images_path = dirPath + '/images'
cap = cv2.VideoCapture(os.path.join(dirPath, config.VIDEO_NAME))
count = 1

if os.path.exists(images_path):
    shutil.rmtree(images_path)
os.makedirs(images_path)

# 循环读取视频的每一帧
while True:
    ret, frame = cap.read()    
    if ret:
        if(count % config.FRAME_CD == 0):
            print('the number of frames:' + str(count))
            # 保存截取帧到本地
            cv2.imwrite(images_path + '/frame' + str(count) + '.jpg', frame)
        count += 1
        cv2.waitKey(0)
    else:
        print('frames were created successfully')
        break

cap.release()

这里使用opencv提取视频的关键帧图片并保存在当前目录images文件夹下。

1.2 通过AI模型提取人物


提取图像中人物的工作需要交给 卷积神经网络 来完成,不同程度的训练对图像分类的准确率影响很大,而这也直接决定了最终的效果。大公司有算法团队来专门训练模型,我们的DEMO使用FACE++提供的开放测试接口,准确率与其付费商用的无异,就是会被限流,失败率高达80%,不过后面我们可以在代码编写中解决这个问题。

# discern.py  ---  调用算法接口返回人体模型灰度图
import os
import shutil
import base64
import re
import json
import threading
import requests
import config

dirPath = os.path.dirname(os.path.abspath(__file__))
clip_path = dirPath + '/clip'

if not os.path.exists(clip_path):
    os.makedirs(clip_path)

# 图像识别类
class multiple_req:
    reqTimes = 0
    filename = None
    data = {
        'api_key': config.FACE_KEY,
        'api_secret': config.FACE_SECRET,
        'return_grayscale': 1
    }

    def __init__(self, filename):
        self.filename = filename

    def once_again(self):
        # 成功率大约10%,记录一下被限流失败的次数 :)
        self.reqTimes += 1
        print(self.filename +' fail times:' + str(self.reqTimes))
        return self.reqfaceplus()

    def reqfaceplus(self):
        abs_path_name = os.path.join(dirPath, 'images', self.filename)
        # 图片以二进制提交
        files = {'image_file': open(abs_path_name, 'rb')}
        try:
            response = requests.post(
                'https://api-cn.faceplusplus.com/humanbodypp/v2/segment', data=self.data, files=files)
            res_data = json.loads(response.text)

            # 免费的API 很大概率被限流返回失败,这里递归调用,一直到这个图片成功识别后返回
            if 'error_message' in res_data:
                return self.once_again()
            else:
                # 识别成功返回结果
                return res_data
        except requests.exceptions.RequestException as e:
            return self.once_again()

# 多线程并行函数
def thread_req(n):
    # 创建图像识别类
    multiple_req_ins = multiple_req(filename=n)
    res = multiple_req_ins.reqfaceplus()
    # 返回结果为base64编码彩色图、灰度图
    img_data_color = base64.b64decode(res['body_image'])
    img_data = base64.b64decode(res['result'])

    with open(dirPath + '/clip/clip-color-' + n, 'wb') as f:
        # 保存彩色图片
        f.write(img_data_color)
    with open(dirPath + '/clip/clip-' + n, 'wb') as f:
        # 保存灰度图片
        f.write(img_data)
    print(n + ' clip saved.')

# 读取之前准备好的所有视频帧图片进行识别
image_list = os.listdir(os.path.join(dirPath, 'images'))
image_list_sort = sorted(image_list, key=lambda name: int(re.sub(r'/D', '', name)))
has_cliped_list = os.listdir(clip_path)
for n in image_list_sort:
    if 'clip-' + n in has_cliped_list and 'clip-color-' + n in has_cliped_list:
        continue
    '''
    为每帧图片起一个单独的线程来递归调用,达到并行效果。所有图片被识别保存完毕后退出主进程,此过程需要几分钟。
    (这里每个线程中都是不断地递归网络请求、挂起等待、IO写入,不占用CPU)
    '''
    t = threading.Thread(target=thread_req, name=n, args=[n])
    t.start()

先读取上文images目录下所有关键帧列表,并为每一个关键帧图片起一个线程,每个线程里创建一个识别类multiple_req的实例,在每个实例里会对当前传入的文件进行不断递归提交识别请求,一直到识别成功为止(请大家自行申请一个免费KEY,我怕face++把我的号封了:)返回识别后的图片保存在clip目录下。<br/>
这个过程因为接口命中成功率很低,同一张图片甚至会反复识别几十次,不过大部分时间都是在等待网络传输和IO读写,所以可以放心大胆地起几百个线程CPU单核都跑不满,等个几分钟全部结果返回脚本会自动退出。

1.2 像素转换、生成轮廓路径


我们之前已经得到了算法帮我们提取后的人关键帧,接下来需要利用opencv来转换像素:<br/>
人物关键帧 to 灰度图 to 黑白反色图 to 轮廓JSON

# translate.py  ---  openCV转换灰度图 & 轮廓判定转换坐标JSON
import os
import json
import re
import shutil
import cv2
import config

dirPath = os.path.dirname(os.path.abspath(__file__))
clip_path = os.path.join(dirPath, 'mask')
cap = cv2.VideoCapture(os.path.join(dirPath, config.VIDEO_NAME))
frame_width = cap.get(cv2.CAP_PROP_FRAME_WIDTH) # 分辨率(宽)
frame_height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT) # 分辨率(高)
FPS = round(cap.get(cv2.CAP_PROP_FPS), 0)   # 视频FPS
mask_cd = int(1000 / FPS * config.FRAME_CD)      # 初始帧时间
milli_seconds_plus = mask_cd  # 每次递增一帧的增加时间
jsonTemp = {                          # 最后要存入的json配置
    'mask_cd': mask_cd,
    'frame_width': frame_width,
    'frame_height': frame_height
}

if os.path.exists(clip_path):
    shutil.rmtree(clip_path)
os.makedirs(clip_path)

# 输出灰度图与轮廓坐标集合
def output_clip(filename):
    global mask_cd
    # 读取原图(这里我们原图就已经是灰度图了)
    img = cv2.imread(os.path.join(dirPath, 'clip', filename))
    # 转换成灰度图(openCV必须要转换一次才能喂给下一层)
    gray_in = cv2.cvtColor(img , cv2.COLOR_BGR2GRAY)
    # 反色变换,gray_in为一个三维矩阵,代表着灰度图的色值0~255,我们将黑白对调
    gray = 255 - gray_in
    # 将灰度图转换为纯黑白图,要么是0要么是255,没有中间值
    _, binary = cv2.threshold(gray , 220 , 255 , cv2.THRESH_BINARY)
    # 保存黑白图做参考
    cv2.imwrite(clip_path + '/invert-' + filename, binary)
    # 从黑白图中识趣包围图形,形成轮廓数据
    contours, _ = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    # 解析轮廓数据存入缓存
    clip_list = []
    for item in contours:
        if item.size > 0:
            # 每个轮廓是一个三维矩阵,shape为(n, 1, 2) ,n为构成这个面的坐标数量,1没什么意义,2代表两个坐标x和y
            rows, _, __ = item.shape
            clip = []
            clip_list.append(clip)
            for i in range(rows):
                # 将np.ndarray转为list,不然后面JSON序列化解析不了
                clip.append(item[i, 0].tolist())

    millisecondsStr = str(mask_cd)
    # 将每一个轮廓信息保存到key为帧所对应时间的list
    jsonTemp[millisecondsStr] = clip_list

    print(filename + ' time(' + millisecondsStr +') data.')
    mask_cd += milli_seconds_plus

# 列举刚才算法返回的灰度图
clipFrame = []
for name in os.listdir(os.path.join(dirPath, 'clip')):
    if not re.match(r'^clip-frame', name):
        continue
    clipFrame.append(name)

# 对文件名进行排序,按照帧顺序输出
clipFrameSort = sorted(clipFrame, key=lambda name: int(re.sub(r'/D', '', name)))
for name in clipFrameSort:
    output_clip(name)

# 全部坐标提取完成后写成json提供给flutter
jsObj = json.dumps(jsonTemp)

fileObject = open(os.path.join(dirPath, 'res.json'), 'w')
fileObject.write(jsObj)
fileObject.close()

print('calc done')

对每一个人物关键帧进行计算,这里就是一层层的像素操作。opencv会把图片像素点生成numpy三维矩阵,计算速度快,操作起来便捷,比如我们要把一个三维矩阵gray_in的灰度图黑白像素对换,只需要gray = 255 - gray_in就可以得到一个新的矩阵而不需要用python语言来循环。<br/>
最后把计算出的帧的闭包图形路径转换为普通的多维数组类型并存入配置文件Map<key, value>key为视频的进度时间msvalue为闭包路径(就是图中白色区域的包围路径,排除黑色人物区域),是一个二维数组,因为一帧里会有n个闭包路径组成。另外还要将视频信息存入配置文件,其中frame_cd就是告诉flutter每间隔多少ms切换下一帧蒙版,视频的宽高分辨率用于flutter初始化播放器自适应布局。<br/>
具体JSON数据结构可见上方图片。现在我们已经得到了一个res.json的配置文件,里面包含了该视频关键帧数据的裁剪坐标集,接下来就用flutter去剪纸吧~

2. Flutter前端

2.1 弹幕调度动画组

弹幕调度系统各端实现都大同小异,只是动画库的API方式区别。flutter里使用SlideTransition可以实现单条弹幕文字的动画效果。

// core.dart --- 单条弹幕动画
class Barrage extends StatefulWidget {
  final BarrageController barrageController;
  Barrage(this.barrageController, {Key key}) : super(key: key);

  @override
  _BarrageState createState() => _BarrageState();
}

class _BarrageState extends State<Barrage> with TickerProviderStateMixin {
  AnimationController _animationController;
  Animation<Offset> _offsetAnimation;
  _PlayPauseState _playPauseState;

  void _initAnimation() {
    final barrageController = widget.barrageController;

    _animationController = AnimationController(
      value: barrageController.value.scrollRate,
      duration: barrageController.duration,
      vsync: this,
    );

    _animationController.addListener(() {
      barrageController.setScrollRate(_animationController.value);
    });

    _offsetAnimation = Tween<Offset>(
      begin: const Offset(1.0, 0.0),
      end: const Offset(-1.0, 0.0),
    ).animate(_animationController);

    _playPauseState = _PlayPauseState(barrageController)
      ..init()
      ..addListener(() {
        _playPauseState.isPlaying ? _animationController.forward() : _animationController.stop(canceled: false);
      });

    if (_playPauseState.isPlaying) {
      _animationController.forward();
    }
  }

  void _disposeAnimation() {
    _animationController.dispose();
    _playPauseState.dispose();
  }

  @override
  void initState() {
    super.initState();
    _initAnimation();
  }

  @override
  void didUpdateWidget(Barrage oldWidget) {
    super.didUpdateWidget(oldWidget);
    _disposeAnimation();
    _initAnimation();
  }

  @override
  void deactivate() {
    _disposeAnimation();
    super.deactivate();
  }

  @override
  Widget build(BuildContext context) {
    return SlideTransition(
      position: _offsetAnimation,
      child: SizedBox(
        width: double.infinity,
        child: widget.barrageController.content,
      ),
    );
  }
}

当有海量弹幕来袭时,首先需要在播放器上层的Container容器中创造多个弹幕通道,并通过算法调度每一个弹幕该出现在哪个通道,初始化动画,并在移除屏幕后dispose动画并移除该条弹幕的Widget

在此基础上,还需要设置一个时间的随机性,让每一条弹幕动画的飘动时间有一个细微的差异,以此来优化整体弹幕流的视觉效果。关于弹幕调度详细代码可参考此项目core.dart文件。这里便不做详述。

2.2 裁剪蒙版容器
// main.dart (部分代码) ---  初始化时引入配置文件
class Index extends StatefulWidget {
  //...
}
class IndexState extends State<Index> with WidgetsBindingObserver {
  //...
  Map cfg;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    Future<String> loadString = DefaultAssetBundle.of(context).loadString("py/res.json");

    loadString.then((String value) {
      setState(() {
        cfg = json.decode(value);
      });
    });
  }
  //...
  //...
}

正式环境肯定是从网络http长连接或者socket获取实时数据,由于我们是离线演示DEMO,方便起见需要在初始化时加载刚才后端产出蒙版路径res.json打包到APP中。

// barrage.dart (部分代码) ---  裁剪蒙版容器
class BarrageInit extends StatefulWidget {
  final Map cfg;
  const BarrageInit({Key key, this.cfg}) : super(key: key);

  @override
  BarrageInitState createState() => BarrageInitState();
}
class BarrageInitState extends State<BarrageInit> {
  //...
  BarrageWallController _controller;
  List curMaskData;

  //...
  @override
  Widget build(BuildContext context) {
    num scale = MediaQuery.of(context).size.width / widget.cfg['frame_width'];
    return ClipPath(
      clipper: curMaskData != null ? MaskPath(curMaskData, scale) : null,
      child: Container(
        color: Colors.transparent,
        child: _controller.buildView(),
      ),
    );
  }
}

class MaskPath extends CustomClipper<Path> {
  List<dynamic> curMaskData;
  num scale;

  MaskPath(this.curMaskData, this.scale);

  @override
  Path getClip(Size size) {
    var path = Path();
    curMaskData.forEach((maskEach) {
      for (var i = 0; i < maskEach.length; i++) {
        if (i == 0) {
          path.moveTo(maskEach[i][0] * scale, maskEach[i][1] * scale);
        } else {
          path.lineTo(maskEach[i][0] * scale, maskEach[i][1] * scale);
        }
      }
    });

    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) {
    return true;
  }
}

flutter实现蒙版效果的核心就在于CustomClipper类,它允许我们通过Path对象来自定义坐标绘制一个裁剪路径(类似于canvas绘图),我们创建一个MaskPath,并在里面绘制我们刚才加载的配置文件的那一帧,然后通过ClipPath包裹弹幕外层容器,就可以实现一个剪裁蒙版的效果:

这里加背景色为了看的更清楚,后续我们会把Container背景颜色设置为Colors.transparent

2.3 视频流蒙版同步

首先我们需要引入一个播放器,考虑到IOS和Android插件的稳定性,我们用flutter官方提供的播放器插件video_player

// video.dart (部分代码) ---  监听播放器进度重绘蒙版
class VedioBg extends StatefulWidget {
  //...
}
class VedioBgState extends State<VedioBg> {
  VideoPlayerController _controller;
  Future _initializeVideoPlayerFuture;
  bool _playing;
  num inMilliseconds = 0;
  Timer timer;

  //...

  @override
  void initState() {
    super.initState();
    int cd = widget.cfg['mask_cd'];
    _controller = VideoPlayerController.asset('py/source.mp4')
      ..setLooping(true)
      ..addListener(() {
        final bool isPlaying = _controller.value.isPlaying;
        final int nowMilliseconds = _controller.value.position.inMilliseconds;
        if ((inMilliseconds == 0 && nowMilliseconds > 0) || nowMilliseconds < inMilliseconds) {
          timer?.cancel();
          int stepsTime = (nowMilliseconds / cd).round() * cd;
          timer = Timer.periodic(Duration(milliseconds: cd), (timer) {
            stepsTime += cd;
            eventBus.fire(ChangeMaskEvent(stepsTime.toString()));
          });
        }
        inMilliseconds = nowMilliseconds;
        _playing = isPlaying;
      });

    _initializeVideoPlayerFuture = _controller.initialize().then((_) {});
    _controller.play();
  }

  //...
}

在video初始化后,通过addListener开始监听播放进度。当播放进度改变时候,获取当前的进度毫秒,去寻找与当前进度最接近的配置文件中的数据集stepsTime,这个配置的蒙版就是当前播放画面帧的裁剪蒙版,此时立刻通过eventBus.fire通知蒙版容器用keystepsTime的数组路径进行重绘。校准蒙版。<br/>
这里实际操作中会遇到两个问题:<br/>

  1. 如何确定当前的进度离哪一帧数据集最近?<br/>

    • 答:在之前数据准备时,通过计算在配置写入了mask_cd,这个时间是最初提取关键帧的间隔,有了间隔时长就可以通过计算得到int stepsTime = (nowMilliseconds / mask_cd).round() * mask_cd;<br/>
  2. 播放器的回调是500毫秒改变一次时间进度,但是我们要做到极致体验不能有这么久的延迟,否则不能保证画面和蒙版同步<br/>

    • 答:在每次触发进度改变时,新起一个Timer.periodic循环计时器,循环时间就是之前的mask_cd,同时把此刻的进度时间存起来,那么接下来的500毫秒内,即使播放器没有通知我们进度,我们也可以通过不断地累加自行技术,在计时器的回调里调用eventBus.fire通知蒙版重绘校准。切记当视频播放完成并开启循环模式时,要将计时器清除<br/>

到这里已经基本实现了一个Flutter AI弹幕播放器啦~<br/>

3. 拓展

3.1 Web前端实现

web前端实现要比native实现简单,这里稍微提及一下。服务端处理数据流程是不变的,但是如果只需要对接web前端,就不用将灰度图转换为json配置。这得益于webkit浏览器内核帮我们做了很多工作。


从哔哩哔哩网站中审查元素上就可以看到,在播放器<video>元素上有一层弹幕蒙版<div>,这个蒙版设置了一个-webkit-mask-image的CSS属性,传入我们之前生成的灰度图片,浏览器内部会帮我们挖出一个蒙版,省去了我们自己去计算轮廓的步骤,canvassvg也有的API可以实现这个效果,但是无疑CSS是最简单的。

3.2 视频点播与直播

其实对于蒙版弹幕来讲本质上没有区别,因为视频网站不可能吧一整个视频编码为mp4格式放给用户,都是通过长连接返回m4sflv的视频切片给用户,所以直播点播都一样。蒙版弹幕的配置信息,不管是web端的base64图片,还是app需要的坐标点json,都需要跟随视频切片一起编码为二进制流,拉到端内再解码,视频的部分喂给播放器,蒙版信息单独抽出来。这两部分得在一个数据包,如果分开传输,就会造成画面蒙版不同步的问题。<br/>
在直播场景中,视频上传到云端需要实时地提取关键帧,进行图像识别分类,最后再编码推给用户端,这个过程需要时间,所以在开启蒙版弹幕的直播间里会出现延迟,这个是正常的。

3.3 总结

目前flutter缺少稳定开源的多功能播放器插件,官方的插件只具备基本功能,比如直播流切片就无法支持,一些第三方机构的插件又不一定靠得住,也跟不上flutter版本更新的速度,这是目前整个flutter生态存在的问题,导致了要商用就需要投入大量研发成本去开发native插件。<br/>
关于这个AI弹幕播放器DEMO,还有些可优化的细节,比如增加蒙版播放器的进度控制,横竖屏切换,特效弹幕等等。文中代码只引入了部分片段,前后端完整代码请参考:<br/>

https://segmentfault.com/a/1190000038320928

一文带你入门 Golang

lei阅读(3)

go 语言特点

脚本化的语法,容易上手。

静态类型+编译性,开发、运行效率都有保证

函数式 & 面向对象 两种编程范式,

原生支持并发编程支持,降低开发成本,维护成本,以及更好的兼容性,效率。

劣势:语法糖没有 Python 和 Ruby 多。运行效率不及C,但已赶超C++,Java。第三方库不多,就是轮子少(喜欢造轮子的可以加入golang轮子大军)。

安装

官方: https://golang.org/

国内官方站点: https://go-zh.org/

Linux

http://golang.org/dl/ 下载最新Go语言二进制包

wget https://dl.google.com/go/go1.13.15.linux-amd64.tar.gz

tar -C /usr/local -xzf go1.13.15.linux-amd64.tar.gz

export PATH=$PATH:/usr/local/go/bin

go version

Mac

环境变量配置

GOROOT, GOPATH, GOBIN, PATH, 现在安装的最新golang runtiem都不用配置了环境变量了。

目录结构

go命令

go run

go 代码关键字

break         //退出循环
default     //选择结构默认项(switch、select)
func         //定义函数
interface    //定义接口
select        //channel
case         //选择结构标签
chan         //定义channel
const         //常量
continue     //跳过本次循环
defer         //延迟执行内容(收尾工作)
go         //并发执行
map         //map类型
struct        //定义结构体
else         //选择结构
goto         //跳转语句
package     //包
switch        //选择结构
fallthrough     //??
if         //选择结构
range         //从slice、map等结构中取元素
type        //定义类型
for         //循环
import         //导入包
return         //返回
var        //定义变量

标示符

append bool byte cap close complex complex64 complex128 uint16
copy false float32 float64 imag int int8 int16 uint32
int32 int64 iota len make new nil panic uint64
print println real recover string true uint uint8 uintptr

语言特色

不要求缩进,不要求末尾加分号——;,同一行代码中有多个表达式,需要用 分号 分割。没有使用的变量,包,会导致报错。

每个go源文件开头必须是package开头,定义自己的包

一个目录下,只能有一个包名

一个可执行的文件必须要有 main() 函数

import 引入包

两种引入风格

import "package1"
import "package2"
import (
    "package1"
    pa2 "package2"      // 包别名,别名为 pa2
    . "fmt"
    _ "mysql"
)

. "fmt" 方式引入包的化,使用fmt里面的函数就可直接使用,不用带 fmt 前缀了

如果引入的包不使用,会报错, 或者加个前缀 _ 即可,这样的下划线会把引入的包的init函数执行一下。定义的变量不用,也会报错。

包内初始化函数

定义 包内 初始化函数

func init() {
    
}

只导入这个包部分,并运行init函数,由于导入不全,所以在代码中就不能使用这个包了。

import _ "MyPackage" 

数据类型

序号 类型和描述
1 布尔型 布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true。
2 数字类型 整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。
3 字符串类型: 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。
4 派生类型: 包括: (a) 指针类型(Pointer) (b) 数组类型 (c) 结构化类型(struct) (d) Channel 类型 (e) 函数类型 (f) 切片类型 (g) 接口类型(interface) (h) Map 类型

使用 int 时,根据当前操作系统来的,64位系统对应 int64, 32位操作系统,对应int32.

变量声明

  • 变量声明: var <变量名> [变量类型]
  • 变量赋值: <变量名> = <值,表达式,函数返回>
  • 变量声明赋值:var <变量名> [变量类型] = <值,表达式,函数返回>
  • 变量声明,类型推断,并赋值 <变量名> := <值,表达式,函数返回>
分组声明

var (
    i int
    foo float32
    name string
)

分组批量声明、赋值
var a,b,c,d int = 1,2,3,4
a,b := 1,2

特殊变量 _

变量作用域

  • 函数内定义的变量称为局部变量
  • 函数外定义的变量称为全局变量
  • 全局变量必须使用 var 声明,局部变量可省略

作用域可以分为以下四个类型:

  • 内置作用域:不需要自己声明,所有的关键字和内置类型、函数都拥有全局作用域
  • 包级作用域:必須函数外声明,在该包内的所有文件都可以访问
  • 文件级作用域:不需要声明,导入即可。一个文件中通过import导入的包名,只在该文件内可用
  • 局部作用域:在自己的语句块内声明,包括函数,for、if 等语句块,或自定义的 {} 语句块形成的作用域,只在自己的局部作用域内可用

语句块

语句块是由花括弧({})所包含的一系列语句。

在 Go 中还有很多的隐式语句块:

  • 主语句块:包括所有源码,对应内置作用域
  • 包语句块:包括该包中所有的源码(一个包可能会包括一个目录下的多个文件),对应包级作用域
  • 文件语句块:包括该文件中的所有源码,对应文件级作用域
  • for 、if、switch等语句本身也在它自身的隐式语句块中,对应局部作用域

类型转换

  • 不存在隐式转换,必须是显示
  • 类型转换必须是在两种兼容的类型之间
  • <变量名称> [:]= <目标类型>( <需要转换的变量名> )

类型转换精度丢失

类型断言

断言,顾名思义就是果断的去猜测一个未知的事物。在 go 语言中,interface{} 就是这个神秘的未知类型,其断言操作就是用来判断 interface{} 的类型。

    var foo interface{} = 22

    f, ok := foo.(int)
    if !ok {
        t.Log("Guess wrong ...")
    }
    t.Logf("The type is : %T", f)   

常量

  • 显示 const idenfity [type] = value
  • 隐式 const identify = value () (无类型常量)

变量类型支持: bool, int, float, string

特殊常量 iota

运算

算术运算

运算符 描述 实例
+ 相加 A + B 输出结果 30
相减 A – B 输出结果 -10
* 相乘 A * B 输出结果 200
/ 相除 B / A 输出结果 2
% 求余 B % A 输出结果 0
++ 自增 A++ 输出结果 11
自减 A– 输出结果 9

关系运算

运算符 描述 实例
== 检查两个值是否相等,如果相等返回 True 否则返回 False。 (A == B) 为 False
!= 检查两个值是否不相等,如果不相等返回 True 否则返回 False。 (A != B) 为 True
> 检查左边值是否大于右边值,如果是返回 True 否则返回 False。 (A > B) 为 False
< 检查左边值是否小于右边值,如果是返回 True 否则返回 False。 (A < B) 为 True
>= 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。 (A >= B) 为 False
<= 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。 (A <= B) 为 True

逻辑运算

运算符 描述 实例
&& 逻辑 AND 运算符。 如果两边的操作数都是 True,则条件 True,否则为 False。 (A && B) 为 False
/ / 逻辑 OR 运算符。 如果两边的操作数有一个 True,则条件 True,否则为 False。 (A / / B) 为 True
! 逻辑 NOT 运算符。 如果条件为 True,则逻辑 NOT 条件 False,否则为 True。 !(A && B) 为 True

位运算

运算符 描述 实例
& 按位与运算符”&”是双目运算符。 其功能是参与运算的两数各对应的二进位相与。 (A & B) 结果为 12, 二进制为 0000 1100
/ 按位或运算符”/ “是双目运算符。 其功能是参与运算的两数各对应的二进位相或 (A / B) 结果为 61, 二进制为 0011 1101
^ 按位异或运算符”^”是双目运算符。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 (A ^ B) 结果为 49, 二进制为 0011 0001
<< 左移运算符”<<“是双目运算符。左移n位就是乘以2的n次方。 其功能把”<<“左边的运算数的各二进位全部左移若干位,由”<<“右边的数指定移动的位数,高位丢弃,低位补0。 A << 2 结果为 240 ,二进制为 1111 0000
>> 右移运算符”>>”是双目运算符。右移n位就是除以2的n次方。 其功能是把”>>”左边的运算数的各二进位全部右移若干位,”>>”右边的数指定移动的位数。 A >> 2 结果为 15 ,二进制为 0000 1111

赋值运算

运算符 描述 实例
= 简单的赋值运算符,将一个表达式的值赋给一个左值 C = A + B 将 A + B 表达式结果赋值给 C
+= 相加后再赋值 C += A 等于 C = C + A
-= 相减后再赋值 C -= A 等于 C = C – A
*= 相乘后再赋值 C = A 等于 C = C A
/= 相除后再赋值 C /= A 等于 C = C / A
%= 求余后再赋值 C %= A 等于 C = C % A
<<= 左移后赋值 C <<= 2 等于 C = C << 2
>>= 右移后赋值 C >>= 2 等于 C = C >> 2
&= 按位与后赋值 C &= 2 等于 C = C & 2
^= 按位异或后赋值 C ^= 2 等于 C = C ^ 2
/ = 按位或后赋值 C / = 2 等于 C = C / 2

优先级

优先级 运算符 功能
9 () [] -> . 后缀运算
8 ! *(指针) & ++ — +(正号) -(负号) 单目运算
7 * / % + – 算术运算,加减乘除
6 << >> 位运算
5 == != < <= > >= 逻辑运算、不等、等
4 & / ^ 按位 逻辑与、或
3 / / && 逻辑或、与
2 = += -= *= 等等 赋值运算
1 , 逗号

一元赋值 这两大运算符是 从右到左 关联,其他都是 从左到右 关联。

注意:优先级 值越大则优先级越高。为了方便理解、记忆,我对没有严格按照优先级制表,只是做了个大概!!
更详细的

代码控制语句

if, else, else if

    var number int = 37
    if number += 4; 10 > number {
        fmt.Print("less than 10:", number)
    } else if 10 < number {
        number -= 2
        fmt.Print("greater 10:", number)
    } else {
        
    }

switch, select

package main

import (
    "fmt"
    "math/rand"
)

func main() {
    ia := []interface{}{byte(6), 'a', uint(10), int32(-4), "CC"}
    v := ia[rand.Intn(4)]
    
    // 值 switch
    switch v {
        case 'a' :
            fmt.Println("char: ", v)
        case 10 :
            fmt.Println("uint: ", v)
        case -4 :
            fmt.Println("int: ", v)
        case 0.1 :
            fallthrough
        caes "0.1"
            fmt.Println("float: ", v)
        default :
            fmt.Println("byte: ", v)
    }
    
    // 变量类型 switch
    switch interface{}(v).(type) {
    case string :
        fmt.Printf("Case A.")
    case byte :
        fmt.Printf("Case B.")
    case int :
        fmt.Printf("Case B.")
    default:
        fmt.Println("Unknown!")
    }
}

注意,go语言和其他语言不同的时,每个case代码末尾会自动加上break 操作, 如果你需要使用 fallthrough 来抵消默认的 break

select 用于管道

for

是的 golangforforforeachfor inwhile于一体。

do while 表示:golang你这么绕,不优雅

package main

import (
    "fmt"
    "time"
)

func main() {
    map1 := map[int]string{1: "Golang", 2: "Java", 3: "Python", 4: "C"}
    
    n := 1
    for {   // 省略则默认是true
        if n > 3 {
            break;
        }
        fmt.Println("for true map item: ", map1[n])
        time.Sleep(1)
        n++
    }
    
    for i := 1; i < 4; i++ {
        fmt.Println("for i map item: ", map1[i])
    }
    
    for k,v := range map1 {
        fmt.Print(k, ":", v)
    }
}

goto, break, continue

goto 是跳过代码块

package main

import (
    "fmt"
    "time"
)

func main() {
    code:
        fmt.Println("do some thing~")
        time.Sleep(1)
        
    goto code
}

break 跳出并结束循环

continue 跳过当前循环

虽然不能和PHP那样 break 2 跳出多层, 单只要有goto就能干很多事了。

golang给 循环 就分配了一个 for,语句跳转语句却整了那么多花样

复合数据

内建方法 make & new

内建方法就是不需要引入包就能用的

make 可以创建 slice、map、chan,返回指针类型

  • slice 是可变长的数组
  • map 是key-map 数据数组
  • chan 是go独有的 管道

一股c编程风格扑面而来, char ptr = (char )malloc(sizeof(char) * 5);

内建方法 new

内存置0,返回传入类型的指针地址

package main
import fmt
import reflect

func main() {
    mSlice := make([]string, 3)
    mSlice[0] = "dog"
    mSlice[1] = "cat"
    mSlice[2] = "pig"
    fmt.Println("animals: ", mSlice)
    
    mMap := make(map[int]string)
    mMap[10] = "dog"
    mMap['2'] = "cat"
    fmt.Println(reflect.TypeOf(mMap))
    fmt.Println("animals :: ", mMap)
    
        
    nMap := new(map[int]string)
    fmt.Println(reflect.TypeOf(nMap))
}

append copy delete

slice可以使用copy,append 函数

delete 是专门用来删除 map

  • append(src, ele) 追加元素
  • copy(dst, src) 把src元素赋值到dst上,
  • delete() 删除元素

例子:

package main
import "fmt"

func main() {
    mSlice := make([]string, 3)
    mSlice[0] = "dog"
    mSlice[1] = "cat"
    mSlice[2] = "pig"
    fmt.Println("animals: ", mSlice)

    // append(mSlice, "id-3")   // 这样写会导致报错: append(mSlice, "id-3") evaluated but not used
    mSlice = append(mSlice, "id-3")
    fmt.Println("animals update:", mSlice)
    fmt.Println("animals len :", len(mSlice))
    fmt.Println("animals cap:", cap(mSlice))
    
    // newSlice := make([]string)      // 这样写导致报错:missing len argument to make([]string)
    // newSlice := make([]string, 2)       // 这样写会导致数据丢失2个,不会自动扩容
    newSlice := make([]string, 3)       // 不要多次定义初始化:no new variables on left side of :=
    copy(mSlice, newSlice)          // 这样反向copy,会导致前面的几个数组元素被置为空
    // copy(newSlice, mSlice)
    fmt.Println("animals dst:", mSlice)
    fmt.Println("animals copy:", newSlice)
    
    delete(mMap, 50)
    fmt.Println(mMap)
}

panic & recover

异常处理

panic() 抛出异常

recover() 获取异常

报错会导致程序代码中断,不会再执行后续操作

例子:

package main

import "fmt"
import "errors"

func panicFunc() {
    defer func() {
        // recover()
        message := recover()    // 声明了message 变量就需要使用哦,不然报错
        fmt.Println("panice msg: ", message)
        
        switch message.(type) {
            case string:
            case error:
                fmt.Println("panice error msg: ", message)
            default:
        }
    }()
    // panic("报错啦")
    panic(errors.New("I am error."))
}

func main() {

    panicFunc()
}

len & cap & close

len可以计算 string, array, slice, map, chan
cap 可以计算 slice, map, chan

  • len() 获取数组长度
  • cap() 获取占用空间分配
  • close() 用于关闭管道——chan

当声明一个数组时,go会预先分配一部分空间给当前数组,获取实际空间占用大小,使用cap()

不用像PHP那样,strlen(), count(), length 傻傻分不清楚了。

例子:

package main

import "fmt"

func main() {

    mSlice := make([]string, 3)
    mSlice[0] = "dog"
    mSlice[1] = "cat"
    mSlice[2] = "pig"
    fmt.Println("animals: ", mSlice)

    fmt.Println("animals update:", mSlice)
    fmt.Println("animals len :", len(mSlice))
    fmt.Println("animals cap:", cap(mSlice))

    mChan := make(chan int, 1)
    close(mChan)
    mChan <- 1      // 会导致报错: panic: send on closed channel
}

defer

定一个当前方法关闭时,运行的代码, 压栈设计,先声明的后执行。

结构体

package main

import "fmt"

type Dog struct {
    ID int
    Name string
    Age int32
}

func main() {
    
    var dog Dog
    dog.ID = 1
    dog.Name = "haha"
    dog.Age = 3
    fmt.Println("print Dog Struct", dog)

    dog2 := Dog{ID:2, Name:"san", Age:4}
    fmt.Println("print Dog 2 Struct", dog2)
    
    dog3 := new(Dog)
    dog3.ID = 3
    dog3.Name = "Tom"
    dog3.Age = 5
    fmt.Println("print Dog 3 Struct", dog)
}

输出

print Dog Struct {1 haha 3}
print Dog 2 Struct {2 san 4}
print Dog 3 Struct &{3 Tom 5}

属性 & 函数

接口

/* define an interface */
type interface_name interface {
   method_name1 [return_type]
   method_name2 [return_type]
   ...
   method_namen [return_type]
}

/* define a struct */
type struct_name struct {
   /* variables */
}

/* implement interface methods*/
func (struct_name_variable struct_name) method_name1() [return_type] {
   /* method implementation */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
   /* method implementation */
}

并发

指针

json

需要引入包 encoding/json, 两个函数分别是 json.Marshal(), json.Unmarshal().

注意,最后一个是英文字母小写的L,不是1

json 序列化

package main

import "fmt"
import "encoding/json"

type ServerInfo struct {
    SerName string
    SerIp   string
    SerPort uint16
}

func main() {
    server := new(ServerInfo)
    server.SerName = "http-nginx"
    server.SerIp = "127.0.0.1"
    server.SerPort = 8080
    re,err := json.Marshal(server)
    if nil != err {
        fmt.Println("error: ", err.Error())
    } else {
        fmt.Println("struct json bytes: ", re)
        fmt.Println("struct json string: ", string(re))
    }
    
    mServer := make(map[string]interface{})
    mServer["serverName"] = "apache2-http"
    mServer["serIp"] = "192.168.30.133"
    mServer["serPort"] = "3033"
    mRe,err := json.Marshal(mServer)
    if nil != err {
        fmt.Println("error: ", err.Error())
    } else {
        fmt.Println("map json string: ", string(mRe))
    }
}

输出

struct json bytes:  [123 34 83 101 114 78 97 109 101 34 58 34 104 116 116 112 45 110 103 105 110 120 34 44 34 83 101 114 73 112 34 58 34 49 48 46 49 48 48 46 49 55 46 50 55 58 51 48 48 48 49 34 44 34 83 101 114 80 111 114 116 34 58 56 48 56 48 125]
struct json string:  {"SerName":"http-nginx","SerIp":"10.100.17.27:30001","SerPort":8080}
map json string:  {"serIp":"192.168.30.133","serPort":"3033","serverName":"apache2-http"}

ps: 我也不知道 10.100.17.27:30001 是怎么回事

json 反序列化

可以使用 tag 来做 mapping,

package main

import "fmt"
import "encoding/json"

type ServerInfo struct {
    SerName string  `json:"name"`
    SerIp   string  `json:"ip"`
    SerPort uint16  `json:"port"`
}

func main() {
    // jsonStr := "{/"SerName/":/"http-nginx/",/"SerIp/":/"10.100.17.27:30001/",/"SerPort/":8080}"   // 双引号注意转义
    jsonStr := "{/"name/":/"http-nginx/",/"ip/":/"10.100.17.27:30001/",/"port/":8080}"
    
    sServer := new(ServerInfo)
    jsonBytes := []byte(jsonStr)
    uerr := json.Unmarshal(jsonBytes, &sServer)
    if nil != uerr {
        fmt.Println("error: ", err.Error())
    } else {
        fmt.Println("uns struct: ", sServer)
    }
    
    jsonStr3 := `{"serIp":"192.168.30.133","serPort":"3033","serverName":"apache2-http"}`   // 使用键盘1旁边的 ` 符号包裹双引号就不用转义了
    
    uSer := make(map[string]interface{})
    uErr := json.Unmarshal([]byte(jsonStr3), &uSer)
    if nil != uErr {
        fmt.Println("error: ", uErr.Error())
    } else {
        fmt.Println("unmar map: ", uSer)
    }
}

输出

uns struct:  &{http-nginx 10.100.17.27:30001 8080}
unmar map:  map[serIp:192.168.30.133 serPort:3033 serverName:apache2-http]

tag

tag 这个东东把,就是json的别名,感觉这个功能是go的特色,与encoding/json包紧密结合。

为什么会有这个东西,我估计是这个和 go命名规则 有关,go命名规则,要求public的变量开头要大写,小写开头的变量是private的,所以,json中的变量就会影响一个接口体变量的访问权限,为了不像java那样复杂,提供了方便的tag功能。

package main

import "fmt"
import "encoding/json"

type ServerInfo struct {
    SerName string  `json:"name"`
    SerIp   string  `json:"ip"`
    SerPort uint16  `json:"port"`
}

func main() {
    
    server := new(ServerInfo)
    server.SerName = "http-nginx"
    server.SerIp = "127.0.0.1"
    server.SerPort = 8080
    re,err := json.Marshal(server)
    if nil != err {
        fmt.Println("error: ", err.Error())
    } else {
        fmt.Println("struct json string: ", string(re))
    }
}

输出

struct json string:  {"name":"http-nginx","ip":"10.100.17.27:30001","port":8080}
map json strin

go 特色语法

_

  • _ 变量

这就好比是Linux 里的 /dev/null, 由于go语言要求声明的变量必须被使用,返回的变量必须被接收,那么真有个变量没用但必须要接受怎么办呢,就把返回的参数给他。例如:

package main
import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
    for _, v := range pow {
        fmt.Printf("value is %d/n", v)
    }
}

这里我们只要值,不要key的信息,返回的key不能不收不是,但我也不像把它输出出来,就让 _ 来接收好了。

  • _ 包

引入包, 并不直接使用这个包,运行时执行一次它的 init() 函数,


import (
    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
)

参考

https://segmentfault.com/a/1190000038320643

摸鱼也要有技巧!3个 linux 命令行工具让你假装很忙….

lei阅读(3)

上班摸鱼,怕被抓?

别担心,摸鱼也要有技巧,这 3个 linux 命令行工具可以让你假装很忙!

Genact

我们要看的第一个工具是 Genact。Genact 只是简单地回放你选择的一个序列,在你出去喝茶休息的时候慢慢地、无限期地「编译」你的代码。它播放的序列由你决定,但默认情况下它包括一个加密货币挖掘模拟器、composer php 依赖管理器、内核编译器、下载器、内存管理等等。不过,我最喜欢的是显示 simcity 加载消息的设置。因此,只要没有人检查得太仔细,你就可以花整个下午的时间等待电脑完成进度条。

Genact 有 linux、os x 和 windows 版本,Rust 源码可以在 github 上找到。

Hollywood

Hollywood 采取了更直接的方式。它本质上是在终端中创建一个随机数和拆分屏幕的配置,并启动看起来很忙的应用程序,如 htop、目录树、源代码文件和其他应用程序等,并每隔几秒钟进行切换。它以 shell 脚本的形式组合在一起,因此可以很容易地根据你的需要进行修改。

Hollywood 的源代码可以在 github 上的apache 2.0 许可下找到。

Blessed-contrib

我个人最喜欢的是 Blessed-contrib,但是它并不是一个专门为了这种表演而设计的应用程序。相反,它是一个基于 node.js 的终端仪表板构建库的演示文件,该库名为 fordated contrib。与其他两个命令不同的是,我实际上使用了 fulded contrib 的库来做一些实际工作的事情,而不仅仅是假装工作。它是一个非常有用的库,有可以用一组命令行显示信息的小部件。但它也很容易填充虚拟数据来实现类似《WarGames》中的梦想。

在 github 上可以找到 Blessed-contrib 的源代码。

当然,虽然这些工具很简单,但有很多方法可以让你的屏幕充满各种乱码。你将在电影中看到的最常见的工具之一是 Nmap,这是一个开源的安全扫描器。

当然,你可以创建自己的组合,使用诸如 screen 或 tmux 之类的终端多路复用器来启动你希望的任何程序,显示你想要的数据。

原文:https://opensource.com/articl…
来源:https://linux.cn/article-9591…

image

https://segmentfault.com/a/1190000038319369

MySQL 数据库的优化,你知道有哪些?

lei阅读(5)

前言

数据库优化一方面是找出系统的瓶颈,提高MySQL数据库的整体性能,而另一方面需要合理的结构设计和参数调整,以提高用户的相应速度,同时还要尽可能的节约系统资源,以便让系统提供更大的负荷.

1.优化一览图

2.优化

笔者将优化分为了两大类,软优化和硬优化,软优化一般是操作数据库即可,而硬优化则是操作服务器硬件及参数设置.

2.1 软优化

2.1.1 查询语句优化

  • 1.首先我们可以用EXPLAIN或DESCRIBE(简写:DESC)命令分析一条查询语句的执行信息.
  • 2.例:

`DESC SELECT * FROM user`

显示:其中会显示索引和查询数据读取数据条数等信息.

2.1.2 优化子查询

MySQL中,尽量使用JOIN来代替子查询.因为子查询需要嵌套查询,嵌套查询时会建立一张临时表,临时表的建立和删除都会有较大的系统开销,而连接查询不会创建临时表,因此效率比嵌套子查询高.

2.1.3 使用索引

索引是提高数据库查询速度最重要的方法之一,关于索引可以参高笔者<MySQL数据库索引>一文,介绍比较详细,此处记录使用索引的三大注意事项:

  • LIKE关键字匹配’%’开头的字符串,不会使用索引.
  • OR关键字的两个字段必须都是用了索引,该查询才会使用索引.
  • 使用多列索引必须满足最左匹配.

2.1.4 分解表

对于字段较多的表,如果某些字段使用频率较低,此时应当,将其分离出来从而形成新的表,

2.1.5 中间表

对于将大量连接查询的表可以创建中间表,从而减少在查询时造成的连接耗时.

2.1.6 增加冗余字段 类似于创建中间表,增加冗余也是为了减少连接查询.

2.1.7 分析表,,检查表,优化表

分析表主要是分析表中关键字的分布,检查表主要是检查表中是否存在错误,优化表主要是消除删除或更新造成的表空间浪费.

  • 1.分析表: 使用 ANALYZE 关键字,如ANALYZE TABLE user;
  • Op:表示执行的操作.
  • Msg_type:信息类型,有status,info,note,warning,error.
  • Msg_text:显示信息.
  • 2.检查表: 使用 CHECK关键字,如CHECK TABLE user [option]
  • option 只对MyISAM有效,共五个参数值:
  • QUICK:不扫描行,不检查错误的连接.
  • FAST:只检查没有正确关闭的表.
  • CHANGED:只检查上次检查后被更改的表和没被正确关闭的表.
  • MEDIUM:扫描行,以验证被删除的连接是有效的,也可以计算各行关键字校验和.
  • EXTENDED:最全面的的检查,对每行关键字全面查找.
  • 3.优化表:使用OPTIMIZE关键字,如OPTIMIZE [LOCAL|NO_WRITE_TO_BINLOG] TABLE user;

LOCAL|NO_WRITE_TO_BINLOG都是表示不写入日志.,优化表只对VARCHAR,BLOB和TEXT有效,通过OPTIMIZE TABLE语句可以消除文件碎片,在执行过程中会加上只读锁.

2.2 硬优化

2.2.1 硬件三件套

  • 1.配置多核心和频率高的cpu,多核心可以执行多个线程.
  • 2.配置大内存,提高内存,即可提高缓存区容量,因此能减少磁盘I/O时间,从而提高响应速度.
  • 3.配置高速磁盘或合理分布磁盘:高速磁盘提高I/O,分布磁盘能提高并行操作的能力.

2.2.2 优化数据库参数

优化数据库参数可以提高资源利用率,从而提高MySQL服务器性能.MySQL服务的配置参数都在my.cnf或my.ini,下面列出性能影响较大的几个参数.

  • key_buffer_size:索引缓冲区大小
  • table_cache:能同时打开表的个数
  • query_cache_size和query_cache_type:前者是查询缓冲区大小,后者是前面参数的开关,0表示不使用缓冲区,1表示使用缓冲区,但可以在查询中使用SQL_NO_CACHE表示不要使用缓冲区,2表示在查询中明确指出使用缓冲区才用缓冲区,即SQL_CACHE.
  • sort_buffer_size:排序缓冲区

2.2.3 分库分表

因为数据库压力过大,首先一个问题就是高峰期系统性能可能会降低,因为数据库负载过高对性能会有影响。另外一个,压力过大把你的数据库给搞挂了怎么办?所以此时你必须得对系统做分库分表 + 读写分离,也就是把一个库拆分为多个库,部署在多个数据库服务上,这时作为主库承载写入请求。然后每个主库都挂载至少一个从库,由从库来承载读请求。

2.2.4 缓存集群

如果用户量越来越大,此时你可以不停的加机器,比如说系统层面不停加机器,就可以承载更高的并发请求。然后数据库层面如果写入并发越来越高,就扩容加数据库服务器,通过分库分表是可以支持扩容机器的,如果数据库层面的读并发越来越高,就扩容加更多的从库。但是这里有一个很大的问题:数据库其实本身不是用来承载高并发请求的,所以通常来说,数据库单机每秒承载的并发就在几千的数量级,而且数据库使用的机器都是比较高配置,比较昂贵的机器,成本很高。如果你就是简单的不停的加机器,其实是不对的。所以在高并发架构里通常都有缓存这个环节,缓存系统的设计就是为了承载高并发而生。所以单机承载的并发量都在每秒几万,甚至每秒数十万,对高并发的承载能力比数据库系统要高出一到两个数量级。所以你完全可以根据系统的业务特性,对那种写少读多的请求,引入缓存集群。具体来说,就是在写数据库的时候同时写一份数据到缓存集群里,然后用缓存集群来承载大部分的读请求。这样的话,通过缓存集群,就可以用更少的机器资源承载更高的并发。建议收藏备查!MySQL 常见错误代码说明

结语

一个完整而复杂的高并发系统架构中,一定会包含:各种复杂的自研基础架构系统。各种精妙的架构设计.因此一篇小文顶多具有抛砖引玉的效果,但是数据库优化的思想差不多就这些了.

来源:https://segmentfault.com/a/11…

image

https://segmentfault.com/a/1190000038318954

聊聊golang的类型断言

lei阅读(3)

本文主要研究一下golang的类型断言

类型断言

x.(T)
  • 断言x不为nil且x为T类型
  • 如果T不是接口类型,则该断言x为T类型
  • 如果T类接口类型,则该断言x实现了T接口

实例1

func main() {
    var x interface{} = 7
    i := x.(int)
    fmt.Println(reflect.TypeOf(i))
    j := x.(int32)
    fmt.Println(j)
}

直接赋值的方式,如果断言为true则返回该类型的值,如果断言为false则产生runtime panic;j这里赋值直接panic

输出

int
panic: interface conversion: interface {} is int, not int32

goroutine 1 [running]:
main.main()
        /Users/caibosi/workspace/go-parent/basic-demo/type_assertion.go:12 +0xda
exit status 2

不过一般为了避免panic,通过使用ok的方式

func main() {
    var x interface{} = 7
    j, ok := x.(int32)
    if ok {
        fmt.Println(reflect.TypeOf(j))
    } else {
        fmt.Println("x not type of int32")
    }
}

switch type

另外一种就是variable.(type)配合switch进行类型判断

func main() {
    switch v := x.(type) {
    case int:
        fmt.Println("x is type of int", v)
    default:
        fmt.Printf("Unknown type %T!/n", v)
    }
}

判断struct是否实现某个接口

type shape interface {
    getNumSides() int
    getArea() int
}
type rectangle struct {
    x int
    y int
}

func (r *rectangle) getNumSides() int {
    return 4
}
func (r rectangle) getArea() int {
    return r.x * r.y
}

func main() {
    // compile time Verify that *rectangle implement shape
    var _ shape = &rectangle{}
    // compile time Verify that *rectangle implement shape
    var _ shape = (*rectangle)(nil)

    // compile time Verify that rectangle implement shape
    var _ shape = rectangle{}
}

输出

cannot use rectangle literal (type rectangle) as type shape in assignment:
        rectangle does not implement shape (getNumSides method has pointer receiver)

小结

  • x.(T)可以在运行时判断x是否为T类型,如果直接使用赋值,当不是T类型时则会产生runtime panic
  • 使用var _ someInterface = someStruct{}可以在编译时期校验某个struct或者其指针类型是否实现了某个接口

doc

https://segmentfault.com/a/1190000038318158