这些天,在给项目servicecomb提交代码,升级其中的vertxnetty版本号,发现有单元测试用例跑不过

https://github.com/apache/servicecomb-java-chassis/pull/2614

1
2
3
4
5
6
@Test
public void testGetMaxFormAttributeSize() {
Assert.assertEquals(8192, TransportConfig.getMaxFormAttributeSize());
ArchaiusUtils.setProperty("servicecomb.rest.server.maxFormAttributeSize", 3072);
Assert.assertEquals(3072, TransportConfig.getMaxFormAttributeSize());
}

通过这个用例可以拦截,如果新版本TransportConfig中依赖的底层默认值修改为非8192的问题。

对于底层依赖的其他项目,有些参数有统一的文件放置,甚至有些项目,参数分散在各个文件。相比升级后,逐个参数检查,通过单元测试能更有效的发现默认值的变化或移动再处理。我觉得这是一个很好的实践。推广到非library库中,我们维护的pulsar、mysql等组件,也可以通过ut测试来保证一些配置文件中的默认参数没有新增、删除或修改。

这是近期观摩某个产品安全加固时的心得,也请教过我们部门的安全专家、架构师。第一篇文章,我想先分析一下重要性和我心中的实现原则。后面可以贴出一些更详细的流程和代码样例

早期没有做好安全,后果很严重

李林峰老师,也就是《Netty权威指南》的作者,同样也在华为工作 :)
。在一本书就曾写过,见过很多产品在安全审核上投入了大量的时间和精力。我表示深深地认同。而这些,往往是因为产品在初期没有做好好的安全设计。以一个假想产品举例,

image-20211031152752639

图中,A与B之间,A与配置中心之前,都存在放置敏感信息的可能。有意思的事情来了,当安全分析发现了A与B之前存在放置敏感信息的可能时,产品决定把A与Mysql之间的通信进行加密和认证,加密的源材料放在哪?决策放到配置中心。可是,当安全分析发现A与配置中心之间存在传递敏感信息的可能时?那产品又应该怎么做?总不能放在Mysql这里吧。

由此可见,如果没有一个完善的从上而下的安全设计,产品在搞安全增强或者说加固的时候,就像哪里破了刷哪里一样,把房间弄得乱七八糟,事倍功半。我相信更难受的是那些刷墙的开发人员。

自上而下安全设计的核心

作为一个开发人员,我还是站在开发人员的角度。我认为对于开发人员的核心就是有一个 证书可轮换、密钥可轮换的基础机制。毕竟开发人员最常碰到的安全问题就是:

  • 通信中敏感数据没有加密
  • 通信中双方没有做认证
  • 敏感信息存储没有加密

拥有了证书可轮换、密钥可轮换的机制,开发人员就可以轻而易举地搞定这三件事情。

从设计、开发人员的思维、底层库开始,证书、密钥都是支持可替换的

  • 底层库支持证书轮换、密钥轮换,同时注意轮换时老密钥的存留时间等
  • 密钥更新时,如果需要彻底废弃老密钥,注意旧数据需要重新解密后用新密钥加密
  • 开发人员也要认同密钥是可替换的,甚至不同用户的密钥可以是不一样的,不然如果平时测试覆盖不到密钥替换,真正实施的话,就会不敢实施,或者出现现网事故

证书、密钥的存储

往往,我们开发的任意两个组件之间,都可能会要求互相通信和认证,更不用提那些初始口令了,肯定是需要加密的,所以证书和密钥不适合存在产品自己开发的配置中心,或者是数据库中。更适合在安装部署阶段放置在更底层,更基础设施中,例如

  • 专用的加密硬件
  • 依赖公有云进行部署的,可以将证书存放在云上托管的Kubernetes集群的secret中,并对k8s集群进行加固,避免根密钥、根证书泄露

这些都可以由部署工具自动导入或人工手动导入。

设计证书、密钥的使用轮换机制

  • 证书的粒度:是每个微服务一个证书还是每个业务场景一个证书
  • 密钥的粒度:是每个客户一个工作密钥还是共享一个工作密钥
  • 轮换时是否容忍业务损失,最大可损失时长是多久
  • 还要注意每个微服务能获取到的证书和密钥,它只应该获取它需要的证书、密钥,理论上单一微服务不应该有获取根证书、根密钥的能力。

Cassandra中的Key有如下三种类型

  • Primary Key
  • Partitioning Key
  • Clustering Key

Primary Key 主键

每张表都需要有主键。主键可以是一个字段或者多个字段的组合。每条记录的主键必须唯一。举个例子

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE player (
name text,
club text,
league text,
nationality text,
kit_number text,
position text,
goals int,
assists int,
appearences int,
PRIMARY KEY (club, league, name, kit_number, position, goals)
)

这个数据表的主键有多个字段,称做复合主键。

分区键

Cassandra根据分区键,使用一致性哈希算法,把数据分配到集群的各个机器上。一个机器可以包含多个分区。Cassandra保证同一分区键的数据都在一台机器上。通过合理的设置分区键,可以让你的查询让尽量少的机器处理,提升查询的效率

对于单主键字段来说,分区键和主键是同一个字段。

对于复合主键字段来说,默认情况下,分区键是复合主键的第一个字段。如上例中,分区键是club字段

可以通过括号来将分区键指定为多个字段,如将上面CQL的11行修改为

1
PRIMARY KEY ((name, club), league, kit_number, position, goals)

Clustering Key

Clustering Keys决定了分区内数据的排序。让我们再看一下最初的例子

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE player (
name text,
club text,
league text,
nationality text,
kit_number text,
position text,
goals int,
assists int,
appearences int,
PRIMARY KEY (club, league, name, kit_number, position, goals)
)

在主键中的字段,除了分区键外都是clustering key。既然club是主键,那么league name kit_number position goals是Clustering key。你可以定义clustering key中每个字段的升降序。可以将kit_number降序、goals升序

排序顺序与主键中字段的顺序相同。因此,在上面的例子中,数据是按照如下布局的

  • 所有相同club的运动员都将分在同一个分区
  • 在分区内,按照leauge排序
  • 然后按照name排序
  • 然后按照kit_number排序

定义不同字段升降序的语法如下(默认为升序)

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE player (
name text,
club text,
league text,
nationality text,
kit_number text,
position text,
goals int,
assists int,
appearances int,
PRIMARY KEY (club, league, name, kit_number, position, goals)
)
WITH CLUSTERING ORDER BY (league ASC, name DESC, kit_number ASC, position DESC );

准备被测程序

通过-g编译程序来携带debug信息,这样子输出的错误信息就可以包含精确的行号。如果你可以承受程序运行缓慢,那么我们可以使用-O0来编译程序。如果使用-O1,那么输出的行号可能会不准确。不推荐使用-O2及以上,因为 valgrind memcheck 偶尔会报告不存在的未初始化值错误。

运行程序

如果平时这么运行

1
myprog arg1 arg2

就使用这个命令

1
valgrind --leak-check=yes myprog arg1 arg2

Memcheck是默认的valgrind工具,--leak-check打开了内存泄漏检测开关。

通过这个命令运行,大约会比平时运行慢20到30倍,并且使用更大的内存。Memcheck 将发出它检测到的内存错误和泄漏的信息。

解释memcheck的输出

使用样例的C程序,包含一个内存分配错误和内存溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdlib.h>

void f(void)
{
int* x = malloc(10 * sizeof(int));
x[10] = 0; // problem 1: heap block overrun
} // problem 2: memory leak -- x not freed

int main(void)
{
f();
return 0;
}

执行内存检测

1
2
gcc -g demo.c
valgrind ./a.out

输出信息如下

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
==145== Memcheck, a memory error detector
==145== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==145== Using Valgrind-3.17.0 and LibVEX; rerun with -h for copyright info
==145== Command: ./a.out
==145==
==145== Invalid write of size 4
==145== at 0x401144: f (demo.c:6)
==145== by 0x401155: main (demo.c:11)
==145== Address 0x4a27068 is 0 bytes after a block of size 40 alloc'd
==145== at 0x484086F: malloc (vg_replace_malloc.c:380)
==145== by 0x401137: f (demo.c:5)
==145== by 0x401155: main (demo.c:11)
==145==
==145==
==145== HEAP SUMMARY:
==145== in use at exit: 40 bytes in 1 blocks
==145== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==145==
==145== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==145== at 0x484086F: malloc (vg_replace_malloc.c:380)
==145== by 0x401137: f (demo.c:5)
==145== by 0x401155: main (demo.c:11)
==145==
==145== LEAK SUMMARY:
==145== definitely lost: 40 bytes in 1 blocks
==145== indirectly lost: 0 bytes in 0 blocks
==145== possibly lost: 0 bytes in 0 blocks
==145== still reachable: 0 bytes in 0 blocks
==145== suppressed: 0 bytes in 0 blocks
==145==
==145== For lists of detected and suppressed errors, rerun with: -s
==145== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
  • 145 是进程号
  • 输出的第一行,即上方的第六行(Invalid write)表明了错误的类型。这里程序往不归它拥有的内存中写入了信息
  • 下方是错误的堆栈信息
  • 代码地址如0x401155通常并不重要,但有时候定位奇怪的问题可能会很关键
  • 一些错误信息会有第二段,用来描述所涉及的内存地址。上例指出写入的内存刚好超过 example.c 的第 5 行使用 malloc() 分配的块的末尾。

内存泄漏信息

1
2
3
4
==145== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==145== at 0x484086F: malloc (vg_replace_malloc.c:380)
==145== by 0x401137: f (demo.c:5)
==145== by 0x401155: main (demo.c:11)

堆栈可以表明什么内存泄露了,但memcheck并不能告诉你内存泄漏的原因

valgrind会提示两种内存泄漏

  • “definitely lost”: 绝对泄漏了内存,必须修复
  • “probably lost”: 程序可能泄漏了内存,也有可能是一些特定的指针操作(如:指针放到了堆中)

参考

https://www.valgrind.org/docs/manual/quick-start.html#quick-start.intro

https://www.valgrind.org/docs/manual/mc-manual.html#mc-manual.errormsgs

前提条件

添加配置

[mysqld]下面添加core-file

image-20211016232603862

ulimit打开core file限制

1
ulimit -c unlimited

如需要,修改core file路径(如在容器内,需要特权容器权限)

1
echo "/opt/sh/mysql/core/core" > /proc/sys/kernel/core_pattern

使得core file携带pid信息

1
echo 1 >/proc/sys/kernel/core_uses_pid

通过kill命令获取core file

1
kill -11 $pid

image-20211016233105059

将程序(以rust程序为例)托管为Systemd运行比较容易,步骤分为以下几步

  • 生成rust二进制文件
  • 创建专用用户和用户组(可省略)
  • 书写.service文件
  • 通过systemctl启动

接下来我们以ubuntu、简单的rust web程序为例,演示一个简单的rust程序如何托管给Systemd运行

生成rust二进制文件

添加依赖

1
2
tokio = { version = "1", features = ["full"] }
warp = "0.3"

书写简单代码

1
2
3
4
5
6
7
8
9
10
11
12
use warp::Filter;

#[tokio::main]
async fn main() {
// GET /hello/warp => 200 OK with body "Hello, warp!"
let hello = warp::path!("hello" / String)
.map(|name| format!("Hello, {}!", name));

warp::serve(hello)
.run(([127, 0, 0, 1], 3030))
.await;
}

编译生成二进制

cargo build --release

二进制就在target/release

创建专用用户和用户组

为了更细粒度地授权和文件权限控制,我们可以给守护程序创建专用的用户

1
sudo useradd vmproxy -s /sbin/nologin -M

书写.service文件

.service文件在ubuntu应该放到/lib/systemd/system/路径下。样例如下vm-proxy.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
[Unit]
Description=Vm Proxy
ConditionPathExists=/home/ubuntu/rust/vm-proxy/target/release/vm-proxy-rust
After=network.target

[Service]
Type=simple
User=vmproxy
Group=vmproxy
LimitNOFILE=1024

Restart=on-failure
RestartSec=10

WorkingDirectory=/home/ubuntu/rust/vm-proxy/target/release
ExecStart=/home/ubuntu/rust/vm-proxy/target/release/vm-proxy-rust

# make sure log directory exists and owned by syslog
PermissionsStartOnly=true
ExecStartPre=/bin/mkdir -p /var/log/vm-proxy
ExecStartPre=/bin/chown syslog:adm /var/log/vm-proxy
ExecStartPre=/bin/chmod 755 /var/log/vm-proxy
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=vm-proxy

[Install]
WantedBy=multi-user.target

ConditionPathExistsWorkingDirectoryExecStart 这三个参数需要根据自己的路径修改

通过Systemctl启动

1
2
sudo systemctl enable vm-proxy.service
sudo systemctl start vm-proxy.service

验证启动完成

image-20211012165631993

环境准备

需要有一个运行的java程序,如果你已经有了运行中的java程序,请跳过这一节,示例,我启动自制的kafka镜像

1
docker run ttbb/kafka:mate

找到java程序的pid

ps -ef或者jps均可,其中jps需要安装jdk

image-20210929143232887

安装arthas

1
2
3
wget https://github.com/alibaba/arthas/releases/download/arthas-all-3.5.4/arthas-bin.zip
mkdir -p arthas
unzip arthas-bin.zip -d arthas

使用arthas连接到目标程序

image-20210929143537663

开始profiler

1
profiler start

如果出现Perf events unavailable. See stderr of the target process.如图所示

image-20210929143621015

需要在docker所在虚拟机上执行如下命令

1
2
echo 1 > /proc/sys/kernel/perf_event_paranoid
echo 0 > /proc/sys/kernel/kptr_restrict

如果是mac用户

1
docker run -it --privileged --pid=host debian nsenter -t 1 -m -u -n -i sh

执行上述命令进入docker所在的虚拟机操作即可

注意,在部分docker版本中,有可能还无法进行profiler采集,您可能需要以特权方式启动容器,不过,为了定位性能问题,这总是值得付出的,不是吗?

image-20210929151106090

等待profiler一段时间

一般等待一分钟即可

结束profiler

1
profiler stop

image-20210929151209352

结束

Congratulations,完成了火焰图的输出,现在你可以使用火焰图来分析执行时间较长的方法啦

image-20210929151506122

今天看到了https://mp.weixin.qq.com/s/tTipcU8MKxTpFurWNEUIww 中热迁移网络句柄的功能,忍不住自己在机器上实验了一下,确实可以实现网络句柄的迁移。实验代码如下

old_server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"net"
"syscall"
)

func main() {
tcpAddr := net.TCPAddr{Port: 8001}
tcpLn, err := net.ListenTCP("tcp4", &tcpAddr)
if err != nil {
panic(err)
}
f, _ := tcpLn.File()
fdNum := f.Fd()
data := syscall.UnixRights(int(fdNum))
// 与新版本sidecar通过Unix Domain Socket建立链接
raddr, _ := net.ResolveUnixAddr("unix", "/dev/shm/migrate.sock")
uds, _ := net.DialUnix("unix", nil, raddr)
// 通过UDS,发送ListenFD到新版本sidecar容器
_, _, _ = uds.WriteMsgUnix(nil, data, nil)
// 停止接收新的request,并且开始排水阶段
tcpLn.Close()
}

new_server

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
package main

import (
"fmt"
"net"
"net/http"
"os"
"syscall"
"time"
)

func main() {
// 新版本sidecar 接收ListenFD,并且开始对外服务
// 监听UDS
addr, _ := net.ResolveUnixAddr("unix", "/dev/shm/migrate.sock")
unixLn, _ := net.ListenUnix("unix", addr)
conn, _ := unixLn.AcceptUnix()
buf := make([]byte, 32)
oob := make([]byte, 32)
time.Sleep(5 * time.Second)
// 接收 ListenFD
_, oobn, _, _, _ := conn.ReadMsgUnix(buf, oob)
scms, _ := syscall.ParseSocketControlMessage(oob[:oobn])
if len(scms) > 0 {
// 解析FD,并转化为 *net.TCPListener
fds, _ := syscall.ParseUnixRights(&scms[0])
f := os.NewFile(uintptr(fds[0]), "")
ln, _ := net.FileListener(f)
tcpLn, _ := ln.(*net.TCPListener)
http.Serve(tcpLn, &MyHttp{})
}
}

type MyHttp struct {
}

func (*MyHttp) ServeHTTP(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "hello world")
}

实验结果

  • 先启动new_server
  • 再启动old_server
  • 然后访问localhost:8001,返回hello world,这是new_server返回的结果

代码地址

https://github.com/hezhangjian/go_demo/tree/main/demo_base/fd_migrate

本篇文章探讨镜像仓库registry的高可用

镜像仓库高可用无单点故障涉及那些场景

镜像仓库对外提供访问无单点故障

镜像仓库对外提供的访问点保持高可用

镜像仓库的数据存储高可用

存储在镜像仓库中的数据都得是高可用的

镜像仓库无单点故障技术关键点

镜像仓库对外提供访问无单点故障

和上一篇文章一样,如果IaaS能提供ELB,我们最好是使用ELB,或者使用浮动IP的方式替换

镜像仓库的数据存储高可用

  • 配置镜像仓库使用IaaS的S3存储
  • 配置镜像仓库使用本地存储,通过共享文件路径存储来实现高可用,如Glusterfs
  • 配置镜像仓库使用S3存储,自建兼容S3 API的存储Server

通常会使用共享存储来做到镜像仓库存储的高可用

方案概述

那么其实镜像仓库的高可用方案就是对上面方案的组合,下面我们举几个例子

镜像仓库依赖组件部署方式

MinIoKeepAlivedregistry都推荐使用容器部署,方便运维管理,但是镜像推荐内置到虚拟机中,不依赖镜像仓库或其他组件,避免循环依赖

使用IaaS的S3存储 + 负载均衡组件

这是最简单的方案,得益于云厂商提供的S3存储和负载均衡组件,我们可以进行很简单的配置,并部署一台以上的registry,如下图所示

kubernetes-registry-ha-s3

自建兼容S3存储 + KeepAlived浮动Ip

我们可以自己搭建MinIo集群来作为兼容S3存储,由于MINIO最低部署4个节点,我们需要根据故障域机器来选择部署MINIO的数目,比如,故障域是三台物理机,我们部署4节点就不妥。原因是,4节点,总会有一台物理机上会部署2个minio节点,如果这台物理机挂掉,就会导致单点故障。所以,如果故障域为三台物理机,我们最好部署6节点,可容忍一台物理机宕机。其他的节点,读者也可以自行测算。

下图是假设三台物理机,minio6副本场景下的部署示意图

kubernetes-registry-ha-minio

注,为了图的美观,并未画出所有的连线

MINIO关键配置

  • 节点数目6个
  • EC2

k8s高可用无单点故障涉及那些场景

k8s 节点添加、pod添加等增删查改无单点故障

需要元数据的存储和处理能力高可用

k8s对外的apiServer(如worker)无单点故障

worker node和其他组件访问apiServer路径高可用

k8s无单点故障技术关键点

元数据存储

通过etcd存储元数据,etcd三节点集群保证高可用

元数据处理

通过多个kube-controllerkube-scheduler节点来保证高可用

worker节点请求数据通过多ip或负载均衡来保证

节点请求通信通过多Ip或负载均衡来保证高可用,这里也有几种方式

IaaS厂商可提供负载均衡的场景下

如下图所示,可将worker node的访问地址指向负载均衡的地址

kubernetes-ha-iaas-lb

私有化部署KeepAlived

私有化部署场景常用keepAlived提供浮动IP来给worker node或其他组件访问,如下图所示

kubernetes-ha-keepalived

私有化部署加上负载均衡组件

如果你觉得同一时刻只有单个apiServer工作会成瓶颈,也可以使用KeepAlivedNginxHaProxy来对ApiServer做负载均衡

kubernetes-ha-keepalived-nginx

为了简化图像,只画出了master1上的Nginx向后转发的场景。

至于Nginx和KeepAlived如何部署,推荐采用容器化的部署模式,方便进行监控和运维;但是镜像不从镜像仓库拉取,而是保存在master节点上,这样虽然升级复杂一点,但是这样子kubernetes的高可用就不依赖镜像仓库了,不会和镜像仓库形成循环依赖,更不会影响镜像仓库的高可用方案,大大简化了后续的技术方案。(因为镜像仓库可能会占据较大的存储空间,可能会和master节点分离部署,这时会作为worker节点连接master节点)。

0%