置顶

为什么会写博客

开始写代码最大的动力就是兴趣初中就喜欢,未曾想到自己的兴趣踩到了时代的红利。

兴趣变成了别人眼中赚钱快的行业,越来越多专业的优秀的985 211大佬涌入了这一行。

诚然专科毕业的我在这一行中并无竞争优势,雨中没伞的孩子只有跑快一点,这个博客的初衷也是

提醒我自己付出的时间和得到的回报应该是成正比的。学习是反人性的,我希望我自己是以学习为

习惯并不是为目的的人,我想记录下我接触过的知识怕遗忘它们,同时也想多年过后对自己说一句

值得。

现状

  • 7年的沪漂游民,经常为待在上海还是重庆做人格分裂的左右互搏。

  • 微小公司技术TL, 5年左右 10 - 20 人 团伙管理经验。靠吃平台成长红利和软性技能赚钱。

  • 很尴尬的瓶颈期,达到了大多目前PHP从业者的绝望期(PHP的上限本来不高,现在大环境更是不好 加上诸如Go java spring 蚕食,泪目)

  • 一个被催着要小孩的中年男人

技能

语言 熟练程度(满分5分) 描述
php 4 能写出工业级产品,设计原理和源码能简单知道点
go 4 能写出工业级产品,设计原理和源码能简单知道点
html 3 给时间就能做,效果和兼容性不是很好
js 3 给时间就能做
python 2 看着文档能干活
c 1 能看懂能改改用,生态不熟悉
java 1 能看懂能改改用,生态不熟悉

未来计划

语言 计划 优先级 补充
html、js 系统性学习 p6 第一梯队,为自己的商业创业项目做准备能写出漂亮的效果
java 系统性学习 p5 第一梯队,一开始学它现在可能收入翻倍了
go 1.阅读更多的开源项目 2.费曼学习录一套GO的基础课程 3.写自己的开源 p4 不会放在第一梯队但是会持续提升
php 放弃提升 p1 市场价位不高,后续乏力

基于双塔模型的AI搜索工业实现

介绍

这篇文章只是介绍CLIP模型的工业实现,能力有限没法深入到具体的算法。目前了解到视觉中国的搜索就是用的这个模型实现的,可以去体验下效果。实现步骤相对简单,记录下来是因为这是最近接触到比较有趣的一个业务场景,同时也感叹下科技爆炸带来的惊叹。

前置知识

  • 双塔模型
    双塔模型如下图,指的是把图片和文字进行编码生成一个512维度的向量,然后比对图文向量得到相关的搜索图片。

  • 向量距离
    向量距离就是说的不同向量的相似度常见的距离算法有。
    1. 余弦相似度
    2. 欧几里得距离
    3. 曼哈顿距离

流程介绍

  1. 团队里面大多技术栈是 PHP + GO 加上环境安装需要解决一些依赖问题。所以把服务端进行了封装我放在了GitHub上 传送门
  2. 图上的ES-PUSH是之前写的一个es推送的小脚本,也是我的第一个Go程序mysql2elasticsearch没有向量类型开了副本提交了一版。
  3. PHP用gRPC的时候会很不顺畅不知道是我的问题还是普片现象,不过我也把grpc_php_plugin打包进了docker环境,如果有缘人遇到了类似的问题可以试试我的思路。

可以看这个dockerfile编译grpc_php_plugin 或者直接用编译好的镜像

1
registry.cn-shanghai.aliyuncs.com/ibaotu/php7.2

在这个环境里没法直接用protoc生成代码,(因为php docker的官方镜像太老了很多依赖包都出了问题)。 我是在PHP8里面调用编译好的grpc_php_plugin生成的。

  1. 再贴一下es的DSL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
PUT /index_name
{
"mappings": {
"properties": {
"id": {
"type": "integer"
},
"title": {
"type": "text"
},
"vector": {
"type": "dense_vector",
"dims": 512
},
"picurl": {
"type": "keyword"
},
"c1": {
"type": "keyword"
}
}
}
}

POST index_name/_search
{
"query": {
"script_score": {
"query": {
"match_all": {}
},
"script": {
"source": "cosineSimilarity(params.queryVector, 'vector') + 1.0",
"params": {
"queryVector": [
...
]
}
}
}
}

}

结尾

本来感觉有很多可以记录下来的,但是回头梳理了下大多耗时都是因为PHP的低版本造成的gRPC使用上的问题解决就行记录下环境编译的过程没什么意义 :-(

最后看下搜索了100w素材后的搜索结果

关键词:夏天

关键词: 沙滩上的美女

关键词: 吃饭的小孩

产考文献

https://modelscope.cn/models/damo/multi-modal_clip-vit-base-patch16_zh/summary

利用阿里云函数FC实现的抠图服务

总结

总结写在前面为了省流。

最后确定实施采用的付费API方案。原因是现在的开源模型在各场景下的表现效果没达到商用标准。但是整个流程是没问题的,展望现在AI的发展速度类似图片分割这类的模型开源也只是时间问题,到时候可以掏出这套流程直接部署线上。

可以后期关注 https://www.modelscope.cn/models/damo/cv_unet_universal-matting/summary

背景

我所在公司任职的项目组想开发抠图功能给用户提供免费的服务,其他项目组应用的第三方抠图服务成本20-30W一年,核心问题就是在抠图效果可接受的前提下需要缩减成本。团队也没有机器视觉处理和训练方面的经验。

模型选择

目前市面上几家巨头都有自己的AI社群,社群里面有训练好的模型并且可以在线体验。
脸书的 huggingface
阿里的 魔塔
百度的 飞浆
分别抽取了这几个社区图像分割热度最高的模型做测试

经过比对最后选定了阿里的damo/cv_swinL_panoptic-segmentation_cocopan模型

技术选型思考

  1. 机器学习首选Python,团队技术栈 PHP/GO为主
  2. 每家社区都有自己的框架,依赖项较多安装繁琐
  3. 网站日活10w用户,峰值qps1K

结合上述3点,我想的是

  1. gRPC对Python逻辑进行封装只处理抠图逻辑,client端调用处理业务逻辑(比如付费限流业务状态熔断等问题)统一开发语言
  2. 抠图服务做成无状态方便动态扩缩容
  3. 动态扩缩容可以选择k8s编排
  4. 考虑到k8s的维护成本和它在公司的普及率,换为阿里云函数

函数计算调研

场景

函数计算可以处理一小段的代码逻辑,触发方式分为事件触发HTTP请求,用于周期性的定时消费任务或者访问量不大的博客类网站有一定的价格优势。gRPC是基于HTTP2.0由此函数计算也兼容了gRPC调用。环境采用镜像的方式部署,代码可以接入各大托管平台。可以根据并发利用率阈值进行动态扩缩容。

目前来看解决了下面的问题

  1. 价格
  2. 容器编排
  3. 通信

计费

资源包 大小 金额(元)
vCPU 50w vCPU*秒 50
内存 100WGB*秒 10
调用函数包 1亿 80
流量 100GB 40
GPU 50wGB*秒 200

计费是多维度的比较玄幻公式是

1
2
vCPU资源使用费用=函数实例vCPU(vCPU)×执行时长(秒)×单价。
内存资源使用费用=函数实例内存(GB)×执行时长(秒)×单价。

实际使用中发现每次调用机器会悬停5-10分钟做为强制预热,期间也是按秒计费的。阿里提供了2中模式。

  1. 最后一次调用悬停10分钟按秒计费(调用1次也是会计费10分钟)
  2. 打开闲置计费强制永久1台机器为闲置状态费用是按秒计费的 1/10

肉眼评估下来用GPU服务任务控制在1s内价格是比直接买服务器来得划算的,并且可负载qps可长可短很鲁棒

DEMO

ProtoBuff

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
syntax="proto3";
option go_package ="../pb";
service ImgHandle {
rpc partition (Request) returns (Reply) {}
}

message Request {
string Url =1;
}


message Reply {
string jsonData = 1;
}

service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from modelscope.pipelines import pipeline
from modelscope.utils.constant import Tasks
from concurrent import futures
import grpc
import json
import pb.partition_pb2 as Impb
import pb.partition_pb2_grpc as IMrpc
import numpy as np

class NumpyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, np.ndarray):
return obj.tolist()
return json.JSONEncoder.default(self, obj)

class ImgHandle(IMrpc.ImgHandleServicer):

def partition(self, request, context):
print(request.Url)
result = segmentor(request.Url)
s = json.dumps(result, cls=NumpyEncoder, ensure_ascii=False)

return Impb.Reply(jsonData=s)

def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))

IMrpc.add_ImgHandleServicer_to_server(ImgHandle(), server)
server.add_insecure_port('0.0.0.0:50051')
print("成功监听")
server.start()
server.wait_for_termination()
if __name__ == '__main__':

segmentor = pipeline(Tasks.image_segmentation, model='damo/cv_swinL_panoptic-segmentation_cocopan')
serve()


client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package main

import (
"bufio"
g "client/grpc"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"image"
_ "image/gif"
_ "image/jpeg"
"image/png"
_ "image/png"
"io"
"net/http"
"os"
"strconv"
"time"
)

const ADDR = "*****:8089"

type T struct {
Masks [][][]int `json:"masks"`
Labels []string `json:"labels"`
Scores []float64 `json:"scores"`
}



func main() {

cred := credentials.NewTLS(&tls.Config{
InsecureSkipVerify: false,
})
url := "https://pic.xxxx.com/01/92/25/82t888piCbj9.jpg"

conn, err := grpc.Dial(ADDR,
grpc.WithTransportCredentials(cred),
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024*1024*1024*20)),
)

if err != nil {
fmt.Println(err)
}

client := g.NewImgHandleClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 50000*time.Second)
defer cancel()
fmt.Println("投递抠图任务")

reply, e := client.Partition(ctx, &g.Request{Url: url})

Down(url)

if e != nil {
fmt.Println(e)
}

var t = T{}
json.Unmarshal([]byte(reply.JsonData), &t)
fmt.Println("抠图处理完成,开始保存图片")
Img(t)

}

func Down(url string) {
client := http.DefaultClient
client.Timeout = 10 * time.Second
reps, err := client.Get(url)
if err != nil {
fmt.Println(err)
}
defer reps.Body.Close()
file, err := os.Create("./temp.png")
if err != nil {
fmt.Println(err)
}
defer file.Close()
w := bufio.NewWriter(file)
io.Copy(w, reps.Body)
w.Flush()
}

func Img(processing T) {

file, err := os.Open("./temp.png")
if err != nil {
panic(any(err))
}
defer file.Close()

atImg, _, e := image.Decode(file)

if e != nil {
fmt.Println(e)
}

wide := atImg.Bounds().Max.X
high := atImg.Bounds().Max.Y



for i, v := range processing.Labels {
resultImg := image.NewRGBA(image.Rect(0, 0, wide, high))
for w := 0; w < wide; w++ {
for h := 0; h < high; h++ {
isShow := processing.Masks[i][h][w]
if isShow == 1 {
resultImg.Set(w, h, atImg.At(w, h))
}
}
}
ImgSave(resultImg, v+"_"+strconv.Itoa(i))
}

}

func ImgSave(img *image.RGBA, name string) {
file, e := os.Create("./" + name + ".png")
if e != nil {
fmt.Println(e)
}
defer file.Close()
b := bufio.NewWriter(file)
defer b.Flush()
png.Encode(b, img)
}

Dockerfile

1
2
3
4
5
6
FROM registry.cn-hangzhou.aliyuncs.com/modelscope-repo/modelscope:ubuntu20.04-py37-torch1.11.0-tf1.15.5-1.2.0
EXPOSE 50051
RUN mkdir -p /usr/local/bin/service
COPY ./service /usr/local/bin/service
RUN python /usr/local/bin/service/docker_init.py #这里需要下载个800M的模型文件,提前打入image
CMD [ "python", "/usr/local/bin/service/main.py" ]

注意点

  1. 打包后的image放在阿里自家服务的同一大区构建会很快
    https://cr.console.aliyun.com/cn-shanghai/instances
  2. 云函数计算gRPC调用是固定端口8089,容器中的监听端口会自动映射上去
  3. 云函数URL触发没有鉴权机制,需要同步开通网关服务
  4. 建议使用相同区域的机器做下流服务通信走vpc,可以不做鉴权和节省流量成本

《设计模式之美》

图像 6

感受

这本书前前后后花了一个半月去阅读,国庆假在重庆休假刚好看完了最后一个设计模式,一句话总结是49买的书值了,并且对我来说时间没白花。

这本书分为2部分,第一部分:面向对象基础,和设计模式的原则 第二部分:设计模式的实现。

收获如下

  • 了解了单元测试的重要性 go的单元测试
  • 对面向对象更加清晰
  • 能理解之前看过一些源码作者的思路
- 大话设计模式 设计模式之美
优点 有UML更直观偏向设计模式的实现 没UML偏向设计模式的使用,有基础铺垫
劣势 场景没带入性 没前者学术

对面向对象与设计模式的看法

前言

写这篇日记的时候楼下的邻居正在装修,沪漂小伙苦不堪言。回想起之前朋友和我抱怨去杭州面试那里的面试官喜欢扣设计模式细节,和我自己3月亲身去重庆面试的经历,我发现很多的面试官真的很问这个并且抠细节,然而他们自己连面向对象是什么都很模糊。为问而问,为用而用,大可不必。

设计模式很重要

我写它的目的不是想证明设计模式不重要,也不是为了批判现在的航母面试。相反我是想佐证设计模式的重要性,但是在我们能真正有资格谈论设计模式前我们至少先对面向对象编程有个清晰的认知,对为什么设计出设计模式和设计的目的有个数。

面向对象

面向对象分为面向对象语言面向对象风格,我们常见的 PHP JAVA Go 等都是面向对象的语言,而 C Js 则不是面向对象语言。面向对象语言面向对象风格的区别是什么呢?

简单来说面向对象需要语言层提供下面的一些特性

面向对象语言的特性

继承

把可以复用的代码抽取出来放到父类提供复用,避免重复的写相同的代码,但是它也经常被诟病。深层级的继承会影响可测试性和可读性。由此产生了多组合少继承的说法,GO就是基于组合的设计思路。

封装

提供了信息隐藏和数据保护,我们只暴露出需要让调用方知道和修改的属性或者方法。

抽象

隐藏内部实现细节,调用方只需要知道调用一个方法是干什么的,而不需要明白是怎么做到的。常见的interfaceabstract是抽象实现的特性,而函数其实也是一种抽象

多态

可以用子类替换父类并可以调用子类的方法。

  • 支持父类对象引用子类对象
  • 支持继承
  • 支持子类重写父类

区别

面向对象语言 从语言层面支持了上述的特性,但是并不是支持了这些特性的语言就是面向对象编程的,因为它的特性可能被人为的破坏掉。
面向对象风格 是指的用面向过程的语言经过巧妙的设计也可以实现出面向对象语言所提供的特性

代码质量

面向过程编程和面向对象编程其实没有谁优于谁的说法,都是时代的产物。在代码量不多的场景下面向过程更适合阅读也可以更快的开发,在大型需要多人协作的场景下面向对象更适合。我们编写代码的时候需要考虑以下的一些衡量代码质量的标准。

可维护性

在不破话原有代码设计不引入新bug的情况下能快速修改或添加代码

可读性

任何人都可以编写计算机能理解的代码,而好的程序员能编写人能理解的代码。这是一个综合的评判比如良好的英文命名风格,模块设计等等。

可扩展性

不修改或者少量修改原有代码的情况下通过扩展的方式添加新的功能

灵活性

预留一定的扩展点,有足够的底层可复用抽象模块

简洁性

尽量保证代码简单减少不必要的炫技行为

可复用性

尽量减少重复代码的编写,复用以后代码

可测试性

可测试性侧面反映了代码质量的高低,比如我前面说到的go单元测试

设计模式的设计原则

设计模式其实只是一些具体的落地措施,实际上的一些设计思想前辈们是总结出了很多原则和套路的,最常见的有下面这些

单一职责

一个类或者模块只负责完成一个职责或者功能

里式代换

子类对象可以在任何地方替换父类对象

开放闭合

对扩展开放,对修改闭合

依赖反转

高层级模块不依赖低层级模块,应该通过抽象相互依赖,抽象不依赖细节,细节依赖抽象。实现依赖反转的场景方法有下面两个

控制反转(IOC)

指的把原来由程序控制的流程反转给了框架,类似Laravel我们只需要根据框架定义好的扩展点去写代码,流程会被框架以list的形式放着一起,一般还会暴露一些操作list的方法。如Laravel的服务提供者就是会把一系列相关的服务放到一个继承了同一个父类的list中暴露出了抽象方法。

依赖注入

在类中去实例化一个对象会造成类与类之间的耦合,这是违反我们设计原则的。依赖注入解决了类与类之间的依赖关系。由外部传入实例好的类对象,是一种基于接口编程的体现。同时依赖注入也为我们写单元测试mock提供了便利。

KISS

Keep It Simple and Stupid.
Keep It Short and Simple.
Keep It Simple and Straightforward
尽量保持简单

YAGNI

You Ain’t Gonna Need It。
不要设计当前用不到的功能

DRY

Don’t Repeat Yourself
不要编写重复代码

LoD

迪米特,高内聚低耦合,
高内聚 类本身的设计,相近的设计尽量放到同一个类中
低耦合 类之间的依赖关系要尽量的简单单一
每个模块只应该了解那些与它关系密切的模块(最少知道原则)

总结

学习坡度

学习设计模式是很有必要的,但是在这之前我们应该知道设计模式的由来,学习的路径建议是

了解面向对象 -> 读懂设计原则 -> 熟悉设计模式

而不是一味的去扣一些设计模式写法上面的细节

带着目的

要搞懂我们为什么要花这些时间去投资学习这些方法论,我的目的是

  1. 能更顺利的读懂框架源码,(Laravel我还想带入设计模式看一遍文档)了解设计思路
  2. 写出高质量的代码

go单元测试

契机

单元测试其实一直是我想做但是没有做的事,写php这么久其实也没怎么写过单测。我给自己找的借口是。

  1. 项目开发工期紧任务重
  2. 项目代码混乱现在已经没法写出单测代码

但是经历了以下的一些事我决定养成良好的写单测的习惯。

  1. 毛剑的 Open Go训练营反复强调了单测
  2. 在给耗子的megaease提pr的时候要求带上单测
  3. 看王峥的《设计模式之美》的时候又反复强调了测试性和重构后的单测

通过上面几件事我认识到想写出不被诟病的好代码除了有良好的 英文命名,优秀的工业设计外还需要覆盖率尽可能高的单元测试

go单元测试

go test 基础

go test 主要功能是提供 单元测试基准测试。我主要记录了单元测试

1
2
3
4
5
6
-bench regexp 执行相应的 benchmarks,例如 -bench=.;
-cover 开启测试覆盖率;
-run regexp 只运行 regexp 匹配的函数,例如 -run=Array 那么就执行包含有 Array 开头的函数;
-v 显示测试的详细命令。
-covermode=count 覆盖率
-coverprofile=count.out 覆盖率详情输出到文件

代码规范

  1. *_test.go 结尾
  2. 函数 Test* 开头

事例

代码

我写了一个用chromedp操作chrome的封装函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package browser

import (
"context"
"errors"
"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp"
"log"
"time"
)

type Browser struct {
ctx context.Context
Cancelfunc context.CancelFunc
}

func (b *Browser) NewBrowser() {
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", true), // 开启窗口模式
chromedp.Flag("window-size", "1920,1080"),
)
b.ctx, b.Cancelfunc = chromedp.NewExecAllocator(context.Background(), opts...)
b.ctx, _ = chromedp.NewContext(b.ctx, chromedp.WithLogf(log.Printf))
}

func (b *Browser) RunGroup (url string , taks ...chromedp.Tasks) error{

actions := chromedp.Tasks{}
actions = append(actions, chromedp.Navigate(url))
for _,v := range taks {
actions = append(actions,v)
}

if err := chromedp.Run(
b.ctx,
actions,
); err != nil {
return err
}
return nil
}

func (b *Browser) Js(js string) chromedp.Tasks {
return chromedp.Tasks{
chromedp.ActionFunc(func(ctx context.Context) error {
_, exp, err := runtime.Evaluate(js).Do(ctx)
if err != nil {
return err
}
if exp != nil {
return exp
}
return nil
}),
}
}

func (b *Browser) SetCookie(host string, cookies ...string) chromedp.Tasks {
if len(cookies)%2 != 0 {
return chromedp.Tasks{
chromedp.ActionFunc(func(ctx context.Context) error {
return errors.New("length of cookies must be divisible by 2")
}),
}
}

return chromedp.Tasks{
chromedp.ActionFunc(func(ctx context.Context) error {
// create cookie expiration
expr := cdp.TimeSinceEpoch(time.Now().Add(180 * 24 * time.Hour))
// add cookies to chrome
for i := 0; i < len(cookies); i += 2 {
err := network.SetCookie(cookies[i], cookies[i+1]).
WithExpires(&expr).
WithDomain(host).
WithHTTPOnly(true).
Do(ctx)
if err != nil {
return err
}
}
return nil
}),
}
}

func (b *Browser) Screenshot(buff *[]byte, quality int) chromedp.Tasks{
return chromedp.Tasks{
chromedp.FullScreenshot(buff,quality),
}
}

然后写了一个测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package browser

import (
"github.com/stretchr/testify/assert"
"testing"
)


func TestBrowser(t *testing.T) {
assert := assert.New(t)

b := Browser{}

b.NewBrowser()
defer b.Cancelfunc()

url := "https://www.baidu.com"
e := b.RunGroup(url)
assert.Nil(e)
e = b.RunGroup("error url")
assert.NotNil(e)


e = b.RunGroup(url, b.Js("console.log(123)"))
assert.Nil(e)
e = b.RunGroup(url, b.Js("-----"))
assert.NotNil(e)

e = b.RunGroup(url, b.SetCookie(url,"zjj","123"))
assert.Nil(e)
e = b.RunGroup(url, b.SetCookie(url,"abc","123","111"))
assert.NotNil(e)

var buff []byte
e = b.RunGroup(url, b.Screenshot(&buff ,100))
assert.Nil(e)

if len(buff) <= 100 {
assert.Fail("image size err")
}

}

查看单测是否通过

覆盖率

查询覆盖率并且输出到文件做后续分析

1
2
go test chromedp*.go --covermode=count
go test chromedp*.go --coverprofile=count.out

1
go tool cover -func ./count.out

1
go tool cover -html ./count.out

相关类库

我个人认为这些库没必要每个都去学习而是用到再看,我了解一些常用的类库比如 testify和monkey考虑到篇幅有限而且它们文档已经很完善了,就没在这边做扩展。

名称 地址 作用
testify gihub assert高频使用,主要提供*testing.T的封装操作
mockery github Golang 的模拟代码自动生成器
httptest 文档 提供http的测试
go-sqlmock github Sql mock driver
miniredis github redis mock
monkey github 汇编语言重写可执行文件的打桩工具

基于阿里云发布案例

ps:除了用阿里用的codeup外其实GitHub支持也很完善,因为我并不打算把代码开源并且我一直用的云效的发布流程部署代码到服务器上,因此此我托管到了阿里code。GitHub 其实有 看见耗子哥用 https://docs.codecov.com 来做单测报告

新建仓库

创建忽略文件,关联远程分支,上传本地代码

新建流水线

在流水线中新建go的单元测试

测试命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apt-get update
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt-get install -y dpkg
dpkg -i ./google-chrome-stable_current_amd64.deb
apt-get install -f -y
dpkg -i ./google-chrome-stable_current_amd64.deb
# 默认使用goproxy.cn用户可手动调整
export GOPROXY=https://goproxy.cn
# 默认的单元测试命令
# 输出测试报告目录到当前工作目录,可自动上传并展示
mkdir -p golang-report
# 未使用Go Mod的用户需要打开一下注释
# export GOFLAGS=-mod=vendor
go test -v -json -cover -coverprofile cover.out ./... > golang-report/report.jsonl
go tool cover -html=cover.out -o golang-report/index.html

我们可以自己设置红线信息然后在后台看见测试报告,如果不符合红线设置代码是无法进入下一个阶段的

接下来的事就是根据自己的需要了,是根据二进制部署还是打包进docker

一些思考

其实现在单元测试不管是PHP还是GO从语言本身和环境生态上都支持得很到位了,我们进行单元测试有几个好处

  • 有效的避免功能迭代或变更时的BUG
  • 控制代码质量(自己都没法写出单测的代码是有问题的
  • 项目不断重构是个过程,单测可以在重构中起到比较好的作用
  • 考虑到更多的边界情况

实践

更新 2022年10月06日01:36:31

看了下这篇日志大概写了1个月了,这是一个月来我对go单元测试的完整流程实践。

GitHub Actions + codecov
https://github.com/zhangjunjie6b/mysql2elasticsearch

产考

http://c.biancheng.net/view/124.html

《大话设计模式》

图像 5

感受

我读这本书的痛点是从业6年写出的代码依旧很烂拿不出手。这本书读了1个月,反反复复读努力想去记住。但是发现没用,它是一本很好的书通过情景带入的方式详细的介绍了23种设计模式 套路是(抛出场景->介绍设计模式->用设计模式解决场景)最后用生动的选美方式做了全书总结,作者很用心是我没用。我的解决方法是买了3本关于代码质量的书(王争的《设计模式之美》,熊节译的《重构》)增加记忆摩擦,听不同大佬对设计模式的理解来让自己的代码变得漂亮。

《虚实之间》

感受

这本书是我2020年买的,2022年掏出来读。原因是听说他因为一些事被判入狱,读完全书后感觉32块定价的书可以听大佬讲述自己的亲身经历还是蛮值的,他优秀到让人妒忌,却又不乏有些惋惜。芮成钢在此书中记载了他在国外经历在达沃斯为国发声,对社会资源分配,奢侈品,中美关系等的一些看法。最喜欢的一句话是 ”人是环境的产物“ ,他的优秀很大部分是因为一个良好的家庭环境 (8亿人只看8个样板戏的年代,他父亲是其中一部的作者)。给子女创造好的环境或者是自己养成一种学习的氛围,这是我们后天每个人都能做到的。

mBART-50模型gRPC封装

背景

事情是这样的,我搬砖的地方每个月都会代理几百上万的海外素材,因为涉及到语言问题都是需要英文转中文的也就是翻译。 翻译是需要成本,我们的做法是做一个中英文的映射关系这样能节约成本但是副作用是丢失语义。

调研后发现 huggingface 有两款机器学习的模型比较试用所以就尝试封装成了一个小的服务。

省流

  1. 仅简单的封装了层gRPC没有深入的调整参数
  2. 基于机器学习效率吃算力,效果看模型。(我是不粘锅)
  3. 效果一般没商业化的翻译精准

效果展示

介绍

facebook/mbart-large-50-one-to-many-mmt 可以将英语翻译成下面提到的其他49种语言。要翻译成目标语言,目标语言id必须作为第一个生成的令牌。要强制将目标语言id作为第一个生成的令牌,请将forced_bos_token_id参数传递给generate方法。

Arabic (ar_AR), Czech (cs_CZ), German (de_DE), English (en_XX), Spanish (es_XX), Estonian (et_EE), Finnish (fi_FI), French (fr_XX), Gujarati (gu_IN), Hindi (hi_IN), Italian (it_IT), Japanese (ja_XX), Kazakh (kk_KZ), Korean (ko_KR), Lithuanian (lt_LT), Latvian (lv_LV), Burmese (my_MM), Nepali (ne_NP), Dutch (nl_XX), Romanian (ro_RO), Russian (ru_RU), Sinhala (si_LK), Turkish (tr_TR), Vietnamese (vi_VN), Chinese (zh_CN), Afrikaans (af_ZA), Azerbaijani (az_AZ), Bengali (bn_IN), Persian (fa_IR), Hebrew (he_IL), Croatian (hr_HR), Indonesian (id_ID), Georgian (ka_GE), Khmer (km_KH), Macedonian (mk_MK), Malayalam (ml_IN), Mongolian (mn_MN), Marathi (mr_IN), Polish (pl_PL), Pashto (ps_AF), Portuguese (pt_XX), Swedish (sv_SE), Swahili (sw_KE), Tamil (ta_IN), Telugu (te_IN), Thai (th_TH), Tagalog (tl_XX), Ukrainian (uk_UA), Urdu (ur_PK), Xhosa (xh_ZA), Galician (gl_ES), Slovene (sl_SI)

项目仓库

quick_start_mbart-50

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
syntax = "proto3";

service MBARTLarge50 {
rpc InterpretE2C (EnglishRequest) returns (ChineseReply) {}
}

message EnglishRequest {
string message = 1;
}


message ChineseReply {
string message = 1;
}

k8s虚拟机环境部署

kubeadm

kubeadm 是 Kubernetes 社区提供的集群构建工具,它负责构建一个最小化可用集群并执行启动等必要的基本步骤,简单来讲,kubeadm 是 Kubernetes 集群全生命周期管理工具,可用于实现集群的部署、升级/降级及卸载等。

kubernetes集群的部署风格主要有2种

  1. 独立组件模式,手动官方下载二进制包
  2. 静态Pod 模式

kubeadm 是个官方安装器,可以方便实现第二种模式的安装,并且提供 init、join、upgrade、reset 等命令管理整个集群

安装先决条件

产考官方文档需要先执行一些配置

说明

宿主机: macos big sur
虚拟机: vmware / Linux 5.15.0-43-generic

1.hostname 重命名

由于 Kubernetes 使用主机名来区分集群里的节点,所以每个节点的 hostname 必须不能重名

1
sudo vi /etc/hostname

2.安装 Docker Engine

1
sudo apt install -y docker.io

3.cgroup -> systemd

目的:更改设置,令容器运行时和 kubelet 使用 systemd 作为 cgroup 驱动,以此使系统更为稳定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

cat <<EOF | sudo tee /etc/docker/daemon.json
{
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m"
},
"storage-driver": "overlay2"
}
EOF

sudo systemctl enable docker
sudo systemctl daemon-reload
sudo systemctl restart docker

4.转发 IPv4 并让 iptables 看到桥接流量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

sudo modprobe overlay
sudo modprobe br_netfilter

# 设置所需的 sysctl 参数,参数在重新启动后保持不变
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF

# 应用 sysctl 参数而不重新启动
sudo sysctl --system

5.关闭 Linux 的 swap 分区

1
2
sudo swapoff -a
sudo sed -ri '/\sswap\s/s/^#?/#/' /etc/fstab

6.kubeadm 安装

官方文档产考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
修改源

sudo apt install -y apt-transport-https ca-certificates curl

curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | sudo apt-key add -

cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list
deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main
EOF

sudo apt update


下载安装

sudo apt install -y kubeadm=1.23.3-00 kubelet=1.23.3-00 kubectl=1.23.3-00


锁死版本
sudo apt-mark hold kubeadm kubelet kubectl

7.下载组件镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//从国内的镜像网站下载然后再用 docker tag 改名

repo=registry.aliyuncs.com/google_containers

for name in `kubeadm config images list --kubernetes-version v1.23.3`; do

src_name=${name#k8s.gcr.io/}
src_name=${src_name#coredns/}

docker pull $repo/$src_name

docker tag $repo/$src_name $name
docker rmi $repo/$src_name
done

安装Master

1
2
3
4
5

sudo kubeadm init \
--pod-network-cidr=10.10.0.0/16 \
--apiserver-advertise-address=192.168.233.128 \
--kubernetes-version=v1.23.3

依次执行

1
2
3
mkdir -p $HOME/.kube 
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

保存

1
2
kubeadm join 192.168.233.128:6443 --token r0ew6o.vdveyxypwco4it1p \
--discovery-token-ca-cert-hash sha256:c41d777b8339d8ba1a8f04698847675a7bf241802c64a401340bfe100888edee

查看节点状态

1
kubectl get node

发现网络状态异常缺少网络插件,集群的内部网络还没有正常运作

安装 Flannel

GitHub: https://github.com/flannel-io/flannel/

我们先下载 flanneld 二进制文件然后修改 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
修改 

/opt/bin/flanneld => "自己的文件路径"
10.244.0.0/16 => 10.10.0.0/16


---
kind: Namespace
apiVersion: v1
metadata:
name: kube-flannel
labels:
pod-security.kubernetes.io/enforce: privileged
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: flannel
rules:
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- apiGroups:
- ""
resources:
- nodes
verbs:
- list
- watch
- apiGroups:
- ""
resources:
- nodes/status
verbs:
- patch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: flannel
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: flannel
subjects:
- kind: ServiceAccount
name: flannel
namespace: kube-flannel
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: flannel
namespace: kube-flannel
---
kind: ConfigMap
apiVersion: v1
metadata:
name: kube-flannel-cfg
namespace: kube-flannel
labels:
tier: node
app: flannel
data:
cni-conf.json: |
{
"name": "cbr0",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
net-conf.json: |
{
"Network": "10.10.0.0/16",
"Backend": {
"Type": "vxlan"
}
}
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kube-flannel-ds
namespace: kube-flannel
labels:
tier: node
app: flannel
spec:
selector:
matchLabels:
app: flannel
template:
metadata:
labels:
tier: node
app: flannel
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/os
operator: In
values:
- linux
hostNetwork: true
priorityClassName: system-node-critical
tolerations:
- operator: Exists
effect: NoSchedule
serviceAccountName: flannel
initContainers:
- name: install-cni-plugin
#image: flannelcni/flannel-cni-plugin:v1.1.0 for ppc64le and mips64le (dockerhub limitations may apply)
image: docker.io/rancher/mirrored-flannelcni-flannel-cni-plugin:v1.1.0
command:
- cp
args:
- -f
- /flannel
- /opt/cni/bin/flannel
volumeMounts:
- name: cni-plugin
mountPath: /opt/cni/bin
- name: install-cni
#image: flannelcni/flannel:v0.19.0 for ppc64le and mips64le (dockerhub limitations may apply)
image: docker.io/rancher/mirrored-flannelcni-flannel:v0.19.0
command:
- cp
args:
- -f
- /etc/kube-flannel/cni-conf.json
- /etc/cni/net.d/10-flannel.conflist
volumeMounts:
- name: cni
mountPath: /etc/cni/net.d
- name: flannel-cfg
mountPath: /etc/kube-flannel/
containers:
- name: kube-flannel
#image: flannelcni/flannel:v0.19.0 for ppc64le and mips64le (dockerhub limitations may apply)
image: docker.io/rancher/mirrored-flannelcni-flannel:v0.19.0
command:
- /root/flanneld
args:
- --ip-masq
- --kube-subnet-mgr
resources:
requests:
cpu: "100m"
memory: "50Mi"
limits:
cpu: "100m"
memory: "50Mi"
securityContext:
privileged: false
capabilities:
add: ["NET_ADMIN", "NET_RAW"]
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: EVENT_QUEUE_DEPTH
value: "5000"
volumeMounts:
- name: run
mountPath: /run/flannel
- name: flannel-cfg
mountPath: /etc/kube-flannel/
- name: xtables-lock
mountPath: /run/xtables.lock
volumes:
- name: run
hostPath:
path: /run/flannel
- name: cni-plugin
hostPath:
path: /opt/cni/bin
- name: cni
hostPath:
path: /etc/cni/net.d
- name: flannel-cfg
configMap:
name: kube-flannel-cfg
- name: xtables-lock
hostPath:
path: /run/xtables.lock
type: FileOrCreate

安装 Flannel 网络

1
kubectl apply -f kube-flannel.yml

再看下网络状态正常

安装worker节点

1
2
sudo kubeadm join 192.168.233.128:6443 --token r0ew6o.vdveyxypwco4it1p \
--discovery-token-ca-cert-hash sha256:c41d777b8339d8ba1a8f04698847675a7bf241802c64a401340bfe100888edee

检查运行状态

1
kubectl get node

产考文档

https://xie.infoq.cn/article/8a4003999a03ebae0e325c412

https://kubernetes.io/zh-cn/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/

https://time.geekbang.org/column/article/534762

请我喝杯咖啡吧~