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

前言

写这篇日记的时候楼下的邻居正在装修,沪漂小伙苦不堪言。回想起之前朋友和我抱怨去杭州面试那里的面试官喜欢扣设计模式细节,和我自己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
  • 控制代码质量(自己都没法写出单测的代码是有问题的
  • 项目不断重构是个过程,单测可以在重构中起到比较好的作用
  • 考虑到更多的边界情况

产考

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

大型网站技术架构

说两句

这本书其实是我3年前读过的一本书,用它来进行第一次视频专栏的尝试其实本质原因是因为我感觉经过3年的知识储备后我有能力讲好它。

2019年的读书笔记

目录

名称 文档 链接 更新时间
介绍 文稿 视频 2022-06-14 12:59:29
演化历程 文稿 视频 2022-06-14 13:32:05
架构套路 文稿 视频 2022-06-16 00:49:29
性能的定义 文稿 视频 2022-08-07 14:16:40

又拍云上传文件漏洞

背景

客服小姐姐突然告诉我,我们网站域名下有一段带颜色的视频。比较奇怪,网站上好像没有开放用户上传MP4的口。排查下来发现是又拍的一个 文件上传漏洞(其实也怪自己没仔细读文档),找了公司对接的又拍售后给到的答复大概意思是 影响大无法修改

知识扩展

上传漏洞定义

文件上传漏洞是指用户上传功能没有做好检验或者过滤不严,用户上传了可执行脚本或者未经约定的文件类型。以往的定义中此类漏洞要一直完成到提权才算一次完整的渗透注入,个人认为只要用户行为与我们约定的行为相违背就可以称为一次成功的操作不一定是需要提权。拿本次事件举例,我们使用CDN并没有提权一说但是公司可能面临管局约谈的风险,并且攻击者达到了自己的目的。

常见手段

限制方法 绕过方法
服务端设置blaklist 寻找list以外的扩展名别名或者混淆大小写
filename后缀白名单 双重后缀 xxx.png.php
MIME类型检测 修改上传http包content-type值
头文件内容检测 修改头部那几个字节让它看着像正常的

又拍上传漏洞复现

我们先计算出form api 需要的 policyauthorization

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
<?php
function sign($key, $secret, $method, $uri, $date, $policy=null, $md5=null)
{
$elems = array();
foreach (array($method, $uri, $date, $policy) as $v)
{
if ($v)
{
$elems[] = $v;
}
}
$value = implode('&', $elems);
//echo $value."\n";
$sign = base64_encode(hash_hmac('sha1', $value, $secret, true));
return 'UPYUN ' . $key . ':' . $sign;
}
function main()
{
$key = '操作员名';
$secret = '密码';
$uri = '/服务空间';
$method = 'POST';
$date = gmdate('D, d M Y H:i:s \G\M\T');
$md = md5($secret);
$mybucket = "phpzjj"; //空间名
$mytime = time() + 600;
$mykey = '/opinion/1.png'; //详情看官方文档
$allow = 'jpg,jpeg,png';//文件类型限制
$limit = 1024000 * 3;//大小限制
$leng = "0,{$limit}";
$option = array('x-upyun-meta-ttl'=>1,'bucket'=>$mybucket,'expiration'=>$mytime,'save-key'=>$mykey,'allow-file-type'=>$allow,'content-length-range'=>$leng,'date'=>$date);
$option = json_encode($option,JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$policy = base64_encode($option);
// 上传,处理,内容识别有存储
echo sign($key, md5($secret), $method, $uri, $date,$policy) . "\n";
echo $policy;
}
main();

然后用 CURL 上传不同的文件

1
2
3
4
5
curl http://v0.api.upyun.com/phpzjj \
-F authorization="UPYUN blogtest:JxWvy+4Wi/THxzpMzW2v6U/vSxg=
" \
-F file=@黑产 \
-F policy=eyJ4LXVweXVuLW1ldGEtdHRsIjoxLCJidWNrZXQiOiJwaHB6amoiLCJleHBpcmF0aW9uIjoxNjU5MjUyMTM1LCJzYXZlLWtleSI6Ii9vcGluaW9uLzEucG5nIiwiYWxsb3ctZmlsZS10eXBlIjoianBnLGpwZWcscG5nIiwiY29udGVudC1sZW5ndGgtcmFuZ2UiOiIwLDMwNzIwMDAiLCJkYXRlIjoiU3VuLCAzMSBKdWwgMjAyMiAwNzoxMjoxNSBHTVQifQ==

正常情况下


符合预期

传一个html试试

符合预期

我们把html换成jpg再看下

已经上传成功并且在又拍的控制台也识别为了html文件(浏览器打开也是html)

当时黑产也是利用这种方式给我们挂了个小黄页,打开这个html文件发现不仅是我们这样的小站,像小红书这样的大站也没幸免 m3u8的播放list就放他们的CND上

这里我找过又拍的企业对接技术,他告诉我们是通过filename的方式来限制文件上传类型的,表示理解但是,既然是filename区分我也确实限制死了文件后缀类型。为什么去掉后缀依然能上传???

不过又拍也是积极响应修改了文档并且增加了 file-ext-required 作为弥补措施

我事后也仔细阅读了又拍的文档

https://help.upyun.com/knowledge-base/form_api/#_2

好的,严格上说起来除了无后缀可上传外其他的好像是我们的问题 :-(

修复建议

对于这个只需要传图的业务场景修复方案其实很简单

  1. 走后端上传,先服务器判断下上传类型 (我都用form api干嘛还要传到自己服务器?
  2. 顺便加个图片处理参数比如 x-gmkerl-quality 又拍会去强验证是否是一张图片(推荐)
  3. 只让上传一种图片格式并且写死content-type

总结

事后我对比其他做CND的厂商,他们的做法其实大体和又拍差不多。唯一不同的是他们有些策略降低了程序员对安全方面的心智负担,个人认为在做一个需求时除非对安全方面知识腌制入味要不很难从文档中读出什么影响安全的猫腻,特别是我这种蹩脚的三流野生PHPER。

oss 测试

1
2
3
policy

{"expiration":"2022-07-31T15:47:50Z","conditions":[["content-length-range",0,20971520],["starts-with","$key","ips_user_asset\/16\/59\/25\/36\/40\/78\/788028ff7b085391fee5967bd982d500.jpg"]]}

不过值得一提的是比如阿里的oss虽然在用户上传时候可以通过修改后缀的方式成功上传一个恶意源文件,但是它返回的content-type头是根据后缀来的而不是读头文件,这样避免了恶意文件被用户直接打开。其实常见的媒体头也没多少,我们做个返回时后缀匹配content-type就可以在大多场景下解决这个问题。

1
2
3
4
5
6
text/html : HTML格式
text/plain :纯文本格式
text/xml : XML格式
image/gif :gif图片格式
image/jpeg :jpg图片格式
image/png:png图片格式

文件后缀 优先级 大于读文件头这种方式更适合直觉,毕竟jpg的后缀给 html的content-type是不合理的。

《自控力》

感受

从我感觉自己有严重的学生综合征起就会有意识的提高自己的自控力,但苦于方式方法是顺从直觉去完成的始终事与愿违。读了这本书后受到了蛮多启发,很多提高自控力的行为都是反自觉的(如书中提到的原谅自己之前的行为及时止损注意以后的行为,白熊测试等)。

请我喝杯咖啡吧~