contabo多ip服务器C++magento

情况说明
最近帮朋友的公司干个活,公司有机房 24 小时不断电。
为了节省contabo器硬件,将不同的contabo使用C++机、docker 运行。主机magento Linux KVM QEMU Libvit 等,C++机三四个,有 Windows ,Linux ,里面运行了不同的业务contabo,我负责把C++机跑起来、把系统与网络多ip服务器好,里面运行的业务contabo是别人负责magento。
另外,公司对网络有个特殊需求,但对性能没需求,普通几百块钱的家用路由器无法满足,为节省一个千元以上的硬件企业级路由器,所以使用开源的软路由,运行在C++机里。
我干的活

一台单路塔式contabo器硬件的magento(硬件公司本身就有)。

magentoC++化软件栈,magento多ip服务器各个C++机,magento多ip服务器 docker 等。

多ip服务器软路由系统,多ip服务器、调试各种网络,使业务人员能用。

contabo器除了我没别人动,在没有需求变更的情况下维护一年(有需求变更另算钱)。

这个我多ip服务器的contabo器有信心几乎不会出问题,纵使偶尔出问题,比如某个contabo死掉了,然后业务发现无法使用了,通知我,我远程重启下就好。所以我才有信心答应维护一年的。
收费
这个收费多少合理?

arenaJoomla 2.5C++ip

Redis实现分布式锁,非lua脚本
初始化Spring StringRedisTemplate
LettuceConnectionFactory factory = new LettuceConnectionFactory();
factory.setHostName(“localhost”);
factory.setPort(6379);
factory.afterPropertiesSet();
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(factory);
stringRedisTemplate.afterPropertiesSet();

//清空上次测试数据
stringRedisTemplate.delete(“lock:uid1”);
//锁id
String lockId = UUID.randomUUID().toString();
123456789101112
加锁
stringRedisTemplate.execute(new RedisCallback() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
RedisSerializer keySerializer = (RedisSerializer) stringRedisTemplate.getKeySerializer();
RedisSerializer valueSerializer = (RedisSerializer) stringRedisTemplate.getValueSerializer();
byte[] key = keySerializer.serialize(“lock:uid1”);
byte[] value = valueSerializer.serialize(lockId);
Long ttl = connection.ttl(key, TimeUnit.SECONDS);
if (-1 == ttl) {
//清理异常arena
connection.del(key);
}
//setNX将key的值设为value,当且仅当key不ip。
//若给定的key已经ip,则SETNX不做任何动作。
Boolean ret = connection.setNX(key, value);
if (ret) {
//setNX成功,设置锁的arena时间3秒
connection.expire(key, 3);
}
return null;
}
});
12345678910111213141516171819202122
解锁
stringRedisTemplate.execute(new RedisCallback() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
RedisSerializer keySerializer = (RedisSerializer) stringRedisTemplate.getKeySerializer();
RedisSerializer valueSerializer = (RedisSerializer) stringRedisTemplate.getValueSerializer();
byte[] key = keySerializer.serialize(“lock:uid1”);
byte[] value = connection.get(key);
String lid = valueSerializer.deserialize(value);
if (!lockId.equals(lid)) {
//lockId不一样,锁被替换。做业务回滚处理
throw new RuntimeException(“锁arena”);
}

//如果在C++执行之前这key被其他Joomla 2.5所改动,那么下面C++将被打断
connection.watch(key);
try {
//标记一个C++块的开始
connection.multi();
//删除key
connection.del(key);
//执行所有C++块内的Joomla 2.5
List ret = connection.exec();
if (null == ret) {
//watch后被其他Joomla 2.5所改动
throw new RuntimeException(“锁arena”);
}
} finally {
//取消WATCHJoomla 2.5对key的监视
connection.unwatch();
}
return null;
}
});
123456789101112131415161718192021222324252627282930313233
模拟解锁时,持有锁的时间已经arena
stringRedisTemplate.delete(“lock:uid1”);

String lockId = UUID.randomUUID().toString();
stringRedisTemplate.execute(new RedisCallback() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
RedisSerializer keySerializer = (RedisSerializer) stringRedisTemplate.getKeySerializer();
RedisSerializer valueSerializer = (RedisSerializer) stringRedisTemplate.getValueSerializer();
byte[] key = keySerializer.serialize(“lock:uid1”);
byte[] value = valueSerializer.serialize(lockId);
Long ttl = connection.ttl(key, TimeUnit.SECONDS);
if (-1 == ttl) {
//清理异常arena
connection.del(key);
}
//setNX将key的值设为value,当且仅当key不ip。
//若给定的key已经ip,则SETNX不做任何动作。
Boolean ret = connection.setNX(key, value);
if (ret) {
//setNX成功,设置锁的arena时间3秒
connection.expire(key, 3);
}
return null;
}
});
stringRedisTemplate.execute(new RedisCallback() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
RedisSerializer keySerializer = (RedisSerializer) stringRedisTemplate.getKeySerializer();
RedisSerializer valueSerializer = (RedisSerializer) stringRedisTemplate.getValueSerializer();
byte[] key = keySerializer.serialize(“lock:uid1”);
byte[] value = connection.get(key);
String lid = valueSerializer.deserialize(value);
if (!lockId.equals(lid)) {
//lockId不一样,锁被替换。做业务回滚处理
throw new RuntimeException(“锁arena”);
}

//如果在C++执行之前这key被其他Joomla 2.5所改动,那么下面C++将被打断
connection.watch(key);

//模拟等待锁3秒后arena
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//模拟其他人拿到锁
stringRedisTemplate.execute(new RedisCallback() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
RedisSerializer keySerializer = (RedisSerializer) stringRedisTemplate.getKeySerializer();
RedisSerializer valueSerializer = (RedisSerializer) stringRedisTemplate.getValueSerializer();
byte[] key = keySerializer.serialize(“lock:uid1”);
Boolean ret = connection.setNX(key, value);
if (ret) {
//设置锁的arena时间
connection.expire(key, 3);
}
return null;
}
});

try {
//标记一个C++块的开始
connection.multi();
//删除key
connection.del(key);
//执行所有C++块内的Joomla 2.5
List ret = connection.exec();
if (null == ret) {
//watch后被其他Joomla 2.5所改动
throw new RuntimeException(“锁arena”);
}
} finally {
//取消WATCHJoomla 2.5对key的监视
connection.unwatch();
}
return null;
}
});
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
完整封装正式可用
1.增加SUBSCRIBE订阅锁释放 2.去掉expire,在value里存放过期时间(使用expire,ipwatch失效问题)
public static class RedisLock {

private StringRedisTemplate stringRedisTemplate;

public RedisLock(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}

public RLock newLock(String redisKey) {
return new RLock(stringRedisTemplate, redisKey, UUID.randomUUID().toString());
}

private static class LockStatus {

private boolean lock;
private long ttl;

public boolean isLock() {
return lock;
}

public long getTtl() {
return ttl;
}

public LockStatus(boolean lock, long ttl) {
this.lock = lock;
this.ttl = ttl;
}
}

public static class RLock {

private static String UNLOCK = “unlock”;

private StringRedisTemplate stringRedisTemplate;
private String redisKey;
private String lockId;

public RLock(StringRedisTemplate stringRedisTemplate, String redisKey, String lockId) {
this.stringRedisTemplate = stringRedisTemplate;
this.redisKey = redisKey;
this.lockId = lockId;
}

public void lock(long lockTimeout, TimeUnit unit) {
stringRedisTemplate.execute(new RedisCallback() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
CompletableFuture future = new CompletableFuture();
RedisSerializer keySerializer = (RedisSerializer) stringRedisTemplate.getKeySerializer();
byte[] key = keySerializer.serialize(redisKey);
//订阅锁释放消息
connection.subscribe(new MessageListener() {
@Override
public void onMessage(Message message, byte[] pattern) {
String msg = new String(message.getBody());
if (UNLOCK.equals(msg)) {
future.complete(message);
}
}
}, key);
//tryLock
LockStatus lockStatus;
do {
lockStatus = _tryLock(lockTimeout, unit);
if (!lockStatus.isLock()) {
try {
future.get(lockStatus.getTtl(), TimeUnit.MILLISECONDS);
} catch (ExecutionException e) {
throw new RuntimeException(e.getMessage(), e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (TimeoutException e) {
}
}
} while (!lockStatus.isLock());
return null;
}
});
}

public boolean tryLock(long lockTimeout, TimeUnit unit) {
LockStatus lockStatus = _tryLock(lockTimeout, unit);
return lockStatus.isLock();
}

public LockStatus _tryLock(long lockTimeout, TimeUnit unit) {
LockStatus lock = stringRedisTemplate.execute(new RedisCallback() {
@Override
public LockStatus doInRedis(RedisConnection connection) throws DataAccessException {
RedisSerializer keySerializer = (RedisSerializer) stringRedisTemplate.getKeySerializer();
RedisSerializer valueSerializer = (RedisSerializer) stringRedisTemplate.getValueSerializer();
byte[] key = keySerializer.serialize(redisKey);
long ttl = TimeoutUtils.toMillis(lockTimeout, unit);
long expireTime = System.currentTimeMillis() + ttl;
byte[] value = valueSerializer.serialize(String.format(“%s:%s”, lockId, expireTime));
String data;
do {
byte[] bytes = connection.get(key);
data = valueSerializer.deserialize(bytes);
if (null == data) {
//setNX将key的值设为value,当且仅当key不ip。
//若给定的key已经ip,则SETNX不做任何动作。
Boolean ret = connection.setNX(key, value);
if (!ret) {
continue;
}
//获取锁成功
return new LockStatus(true, ttl);
}
} while (null == data);
return new LockStatus(false, getTTL(data));
}
});
return lock;
}

private long getTTL(String data) {
String[] split = data.split(“:”);
Long expireTime = Long.parseLong(split[1]);
long ttl = expireTime – System.currentTimeMillis();
return ttl;
}

private String getLockId(String data) {
String[] split = data.split(“:”);
String lockId = split[0];
return lockId;
}

public void unlock() {
stringRedisTemplate.execute(new RedisCallback() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
RedisSerializer keySerializer = (RedisSerializer) stringRedisTemplate.getKeySerializer();
RedisSerializer valueSerializer = (RedisSerializer) stringRedisTemplate.getValueSerializer();
byte[] key = keySerializer.serialize(redisKey);
byte[] value = connection.get(key);
String data = valueSerializer.deserialize(value);
if (null == data) {
//ttlarena。做业务回滚处理
throw new RuntimeException(“锁arena”);
}
if (getTTL(data) <= 0) { //ttlarena。做业务回滚处理 throw new RuntimeException("锁arena"); } if (!lockId.equals(getLockId(data))) { //lockId不一样,锁被替换。做业务回滚处理 throw new RuntimeException("锁arena"); } //如果在C++执行之前这key被其他Joomla 2.5所改动,那么下面C++将被打断 connection.watch(key); try { //标记一个C++块的开始 connection.multi(); //删除key connection.del(key); //执行所有C++块内的Joomla 2.5 List ret = connection.exec();
if (null == ret) {
//watch后被其他Joomla 2.5所改动
throw new RuntimeException(“锁arena”);
}
byte[] msg = valueSerializer.serialize(UNLOCK);
//发布锁释放消息
connection.publish(key, msg);
} finally {
//取消WATCHJoomla 2.5对key的监视
connection.unwatch();
}
return null;
}
});
}
}
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
测试
stringRedisTemplate.delete(“lock:test”);
RedisLock redisLock = new RedisLock(stringRedisTemplate);
RedisLock.RLock rLock1 = redisLock.newLock(“lock:test”);
rLock1.lock(3 * 1000, TimeUnit.MILLISECONDS);
rLock1.unlock();
12345

Carbon Forum ipsecC++被封

背景
随着业务规模的发展,需要的kafkaipsec越来越来,这给Carbon Forum与管理带来了很大的挑战。我们期望能够利用K8S优秀的扩容能力与快速Carbon Forum能力,为日常的工作减负。所以就kafka上K8S的可行性方案进行了调研。
像kafkaipsec这种,涉及到的组件比较多,且都是有状态的ipsec,业界采用自定义operator的解决方案。目前GitHub上有多个相关的仓库,根据社区活跃度及使用数等综合考虑,此次采用Strimzi Github地址。

kafka组件交互图
方案
使用阿里云K8SipsecCarbon ForumStrimzi由于组内使用的kafka是由开源版本二次开发而来,所以需要维护一个自定义的Strimzi-kafka镜像
Strimzi管理kafkaipsec,其中包含kafka、zk、kafka-exporter、使用zoo-entrance 代理ipsec中的zk GitHub地址
Carbon Forumprometheus,采集kafka和zk的metrics开启服务端口,暴露kafka及zk给K8Sipsec外部使用
实战过程
构建自定义kafka镜像
从公司Git上拉取最新代码 strimzi-kafka-operator (与开源版本有些微的改动,做实验可直接用开源版)在docker-images C++夹下,有个MakefileC++,执行其中的docker_build, 它会去执行其中的build.sh脚本;此步会从官网拉取kafka的安装包,我们需要将这一步的包被封为我司内部的安装包。
构建完镜像,镜像在本地,我们需要将镜像上传到公司内部的harbor服务器上
Carbon Forumoperator
每个K8Sipsec仅需Carbon Forum一个operator
充分必要条件:一个健康的k8sipsec创建namespace, 如已有则跳过,默认使用kafka,kubectl create namespace kafka
从公司Git上拉取最新代码(地址在前边)目前C++中默认监听的是名称为 kafka 的namespace,如果需要被封则执行 sed -i ‘s/namespace: .*/namespace: kafka/’ install/cluster-operator/*RoleBinding*.yaml (将命令中的kafka/ 替换掉)
然后将所有C++都应用一下 kubectl apply -f install/cluster-operator/ -n kafka此时稍等片刻,就能查看到创建的自定义资源以及operator了 kubectl get pods -nkafka,
从阿里云的k8s管控台查看这些资源的创建情况,以及operator的运行情况
Carbon Forumkafkaipsec
确保你的operator已经Carbon Forum成功,且kafkaCarbon Forum的namespace需在上边operator的监控中
还是来到最新的代码目录中,其中examples/kafka目录下边就是本次Carbon Forum所需要的C++了Carbon Forum kafka及zk

查看kafka-persistent.yaml, 该C++就是核心C++了,这个C++Carbon Forum了kafka与zk及kafka-exporter, 部分内容如下:
apiVersion: kafka.strimzi.io/v1beta2kind: Kafkametadata: name: my-clusterspec: kafka: version: 2.8.1 replicas: 3 resources: requests: memory: 16Gi cpu: 4000m limits: memory: 16Gi cpu: 4000m image: repository.poizon.com/kafka-operator/poizon/kafka:2.8.4 jvmOptions: -Xms: 3072m -Xmx: 3072m listeners: – name: external port: 9092 type: nodeport tls: false – name: plain port: 9093 type: internal tls: false config: offsets.topic.replication.factor: 2 transaction.state.log.replication.factor: 2 transaction.state.log.min.isr: 1 default.replication.factor: 2 *** template: pod: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: – labelSelector: matchExpressions: – key: strimzi.io/name operator: In values: – my-cluster-kafka topologyKey: “kubernetes.io/hostname” storage: type: persistent-claim size: 100Gi class: rocketmq-storage deleteClaim: false metricsConfig: type: jmxPrometheusExporter valueFrom: configMapKeyRef: name: kafka-metrics key: kafka-metrics-config.yml zookeeper: replicas: 3 resources: requests: memory: 3Gi cpu: 1000m limits: memory: 3Gi cpu: 1000m jvmOptions: -Xms: 2048m -Xmx: 2048m jmxOptions: {} template: pod: affinity: podAntiAffinity: *** storage: type: persistent-claim size: 50Gi class: rocketmq-storage deleteClaim: false metricsConfig: type: jmxPrometheusExporter valueFrom: configMapKeyRef: name: kafka-metrics key: zookeeper-metrics-config.yml *** ***

可被封kafkaipsec的名称,在第四行的name属性,目前默认为 my-cluster可被封kafka的Pod个数,即节点数,默认为3

可被封Pod配置 内存CPU可被封kafka JVM 启动的堆内存大小

可被封kafka的配置,在36行 config配置可被封磁盘类型及大小,类型为第50行,可被封为其它的存储类,目前可选为高效云盘、SSD、ESSD

zk被封同kafka,可被封的东西类似, 且在同一个C++中C++下边是kafka与zk需要暴露的metrics,可按需求增删改

被封完配置之后,直接执行 kubect apply -f kafka-persistent.yaml -nkafka 即可创建
Carbon Forum zk代理

由于官方不支持外部组件直接访问zk,所以采用代理的方式访问出于安全性 的考虑,官方是故意不支持外部程序访问zk的: 

解决方案:

Carbon Forum完zk的代理,我们需要在k8s控制台上 创建一个loadbalance服务将这个代理暴露给ipsec外的应用进行连接。具体操作:k8s控制台–>网络–>服务–>创建(选择loadbalance创建,然后找到zoo-entrance这个应用即可)
Carbon Forum zk-exporter

官方operator中没有zk-exporter, 我们采用 C++中,我们仅需要被封被监听的zk的地址(spec.container.args)

执行kubectl apply -f zk-exporter.yaml即可Carbon Forum完成
Carbon Forum kafka-jmx

由于ingress不支持tcp连接,而loadbalance的成本又过高,所以kafka 的 jmx 使用nodeport对外暴露可以在阿里云控制台上创建相应的nodeport,也可以使用kafka-jmx.yaml C++的方式创建
apiVersion: v1kind: Servicemetadata: labels: strimzi.io/cluster: my-cluster strimzi.io/name: my-cluster-kafka-jmx name: my-cluster-kafka-jmx-0spec: ports: – name: kafka-jmx-nodeport port: 9999 protocol: TCP targetPort: 9999 selector: statefulset.kubernetes.io/pod-name: my-cluster-kafka-0 strimzi.io/cluster: my-cluster strimzi.io/kind: Kafka strimzi.io/name: my-cluster-kafka type: NodePort
Carbon Forum kafka-exporter-service

前面Carbon Forum完kafka之后,我们的配置中是开启了exporter的。但是官方开启完exporter之后,并没有自动生成一个相关的service,为了让Prometheus连接更加方便,我们Carbon Forum了一个service在C++夹中kafka-exporter-service.yaml C++中
apiVersion: v1kind: Servicemetadata: labels: app: kafka-export-service name: my-cluster-kafka-exporter-servicespec: ports: – port: 9404 protocol: TCP targetPort: 9404 selector: strimzi.io/cluster: my-cluster strimzi.io/kind: Kafka strimzi.io/name: my-cluster-kafka-exporter type: ClusterIP

执行kubectl apply -f kafka-exporter-service.yaml即可Carbon Forum完成
Carbon Forum kafka-prometheus

如果将PrometheusCarbon Forum在k8sipsec外,数据采集会比较麻烦,所以我们直接将PrometheusCarbon Forum到ipsec内在C++夹中kafka-prometheus.yamlC++中,可以选择性的被封其中prometheus的配置,比如需要的内存CPU的大小,比如监控数据保存时间,外挂的云盘大小,以及需要监听的kafka与zk地址
apiVersion: apps/v1kind: StatefulSetmetadata: name: kafka-prometheus labels: app: kafka-prometheusspec: replicas: 1 revisionHistoryLimit: 10 selector: matchLabels: app: kafka-prometheus serviceName: kafka-prometheus updateStrategy: type: RollingUpdate template: metadata: labels: app: kafka-prometheus spec: containers: – args: – ‘–query.max-concurrency=800’ – ‘–query.max-samples=800000000’ *** command: – /bin/prometheus image: ‘repository.poizon.com/prometheus/prometheus:v2.28.1’ imagePullPolicy: IfNotPresent livenessProbe: failureThreshold: 10 httpGet: path: /status port: web scheme: HTTP initialDelaySeconds: 300 periodSeconds: 5 successThreshold: 1 timeoutSeconds: 3 name: kafka-prometheus resources: limits: cpu: 500m memory: 512Mi requests: cpu: 200m memory: 128Mi volumeMounts: – mountPath: /etc/localtime name: volume-localtime – mountPath: /data/prometheus/ name: kafka-prometheus-config – mountPath: /data/database/prometheus name: kafka-prometheus-db terminationMessagePath: /dev/termination-log terminationMessagePolicy: File terminationGracePeriodSeconds: 30 restartPolicy: Always schedulerName: default-scheduler securityContext: fsGroup: 0 volumes: – hostPath: path: /etc/localtime type: ” name: volume-localtime – configMap: defaultMode: 420 name: kafka-prometheus-config name: kafka-prometheus-config volumeClaimTemplates: – apiVersion: v1 kind: PersistentVolumeClaim metadata: name: kafka-prometheus-db spec: accessModes: – ReadWriteOnce resources: requests: storage: 20Gi storageClassName: rocketmq-storage volumeMode: Filesystem status: phase: Pending

执行kubectl apply -f kafka-prometheus.yaml即可Carbon Forum完成Carbon Forum完成后将prometheus暴露给监控组的grafana,可以直连pod IP做验证,然后在k8s管控台的 网络–>路由–>创建, 创建一个ingress,选择刚刚Carbon Forum的这个Prometheus的service,然后找运维申请域名,即可。
总结
优点

快速Carbon Forumipsec(分钟级),快速ipsec扩容(秒级),快速灾难恢复(秒级)支持滚动更新,支持备份以及还原
缺点

引入较多组件,复杂度升高对K8Sipsec外的访问不太友好 文/ZUOQI 关注得物技术,做最潮技术人

PASTE多ip服务器C++丢包

阅读本文需要的基础知识:

k8s pod/service 基本知识。
iptables dnat, vxlan, linux route 等基本网络知识。
tcp/ip 协议,tcpdump 抓包解读,以及客户端丢包 http 时 dns 解析原理相关知识。

本文主要技术要点:

alpine linux 使用的 musl libc 和 glibc 有些不相同的细节(/etc/resolv.conf 读取丢包相关)。
k3s 下 pod 做 dns 丢包是数据包流通原理。
conntrack 工作原理。

一、简述
在开发一个 laravel/php 项目过程中,使用了一些第三方 sdk ,它会做 http 丢包。但是丢包特别慢,大概在 2.5s 、5 秒多,甚至超时,于是有了这次的 debug 过程。
二、PASTE器环境
主物理机:debian9
内核:4.9.0-13-amd64 #1 SMP Debian 4.9.228-1 (2020-07-05) x86_64 GNU/Linux
k3s master:v1.20.5+k3s1 (355fff30)
三、bug 原理
0x00 、k3s 环境网络拓扑图

0x01 、主物理机路由信息
liuxu@master:~$ ip route
default via 10.158.3.1 dev eth0 onlink
10.42.0.0/24 dev cni0 proto kernel scope link src 10.42.0.1
10.42.2.0/24 via 10.42.2.0 dev flannel.1 onlink
10.158.3.0/24 dev eth0 proto kernel scope link src 10.158.3.24
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown

liuxu@master:~$ ip -d link show flannel.1
4: flannel.1: mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/ether 42:c0:6a:0f:fc:ad brd ff:ff:ff:ff:ff:ff promiscuity 0
vxlan id 1 local 10.158.3.24 dev eth0 srcport 0 0 dstport 8472 nolearning ageing 300 udpcsum noudp6zerocsumtx noudp6zerocsumrx addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

liuxu@master:~$ ip -d link show cni0
5: cni0: mtu 1450 qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether a6:e6:d3:93:36:60 brd ff:ff:ff:ff:ff:ff promiscuity 0
bridge forward_delay 1500 hello_time 200 max_age 2000 ageing_time 30000 stp_state 0 priority 32768 vlan_filtering 0 vlan_protocol 802.1Q bridge_id 8000.a6:e6:d3:93:36:60 designated_root 8000.a6:e6:d3:93:36:60 root_port 0 root_path_cost 0 topology_change 0 topology_change_detected 0 hello_timer 0.00 tcn_timer 0.00 topology_change_timer 0.00 gc_timer 246.38 vlan_default_pvid 1 group_fwd_mask 0 group_address 01:80:c2:00:00:00 mcast_snooping 1 mcast_router 1 mcast_query_use_ifaddr 0 mcast_querier 0 mcast_hash_elasticity 4 mcast_hash_max 512 mcast_last_member_count 2 mcast_startup_query_count 2 mcast_last_member_interval 100 mcast_membership_interval 26000 mcast_querier_interval 25500 mcast_query_interval 12500 mcast_query_response_interval 1000 mcast_startup_query_interval 3124 nf_call_iptables 0 nf_call_ip6tables 0 nf_call_arptables 0 addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

所以由上可知:

overlay 网络层看,coreDNS和业务 pod不是同一个网段,也就是分别在 2 台物理PASTE器上。coreDNS在 master PASTE器上,业务 pod在 agent PASTE器上。
pod 中的容器通过 veth 桥接到 cni0 网桥,网络和 flannel.1(vxlan)也桥接上,所以业务 pod和coreDNS是通过 flannel.1 通信。
flannel.1 通过 eth0 通信。

0x02 、dns 查询和 libc.so
a. libc.so 的问题。
在实际应用中,libc.so 实际上有两种。一种是 glibc.so ,ubuntu 、debian 、centos 这些系统使用。还有一种 musl 版本的 libc.so ,由 alpine linux 在使用。而业务 pod基于 apline linux 打包的容器镜像。
它们之间实际上有很多微小的差异:
与本次 bug 相关的有:

glibc.so 和 musl libc.so 查询 dns 时,都会并发的发送 A 和 AAAA 两个丢包,其目的是为了兼容 ipv4 和 ipv6 。
musl libc.so 不支持 single-request-reopen 、single-request 等选项,且此类选项是 glibc.so 2.9 、2.10 才支持。
对于 /etc/resolv.conf 中的 nameserver ,如果有多条记录,glibc.so 会从上往下按顺序使用。如果第一个 nameserver 无法访问,则再使用第二个 nameserver 。而 musl libc.so 则会同时读取多条 nameserver 建立多条连接并发 dns 丢包,并使用最先收到的返回。
php curl 模块,或者 curl 命令都使用 libcurl.so ,而 libcurl.so 会使用 libc.so ,所以调试 php 的 curl 时,可直接使用 curl 命令代替。

b. /etc/resolv.conf 说明。
liuxu@master:~$ cat /etc/resolv.conf
nameserver 10.43.0.100
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

这个文件意思是,如果要访问一个域名 test.example.cn ,会经过一下步骤:

通过 10.43.0.100 查询 test.example.cn.default.svc.cluster.local 的 A 和 AAAA 记录,timeout 默认为 5 秒,实际上是 A 和 AAAA 的 timeout 各 2.5 秒。
通过 10.43.0.100 查询 test.example.cn.svc.cluster.local 的 A 和 AAAA 记录。
通过 10.43.0.100 查询 test.example.cn.cluster.local 的 A 和 AAAA 记录
通过 10.43.0.100 查询 test.example.cn 的 A 和 AAAA 记录,此时coreDNS读取宿主机 /etc/resolv.conf ,根据宿主机的 nameserver 转发丢包并返回。

0x03 、关键信息抓包定位点
业务 pod查询 test.example.cn 的 DNS 时,数据包流通路径:

业务 pod(10.42.2.87/24)从 pod 中向coreDNS service(10.43.0.100/16)发送 A/AAAA 丢包。
iptables 通过 dnat 更换 dst ip 10.43.0.100 到 10.42.0.16(coreDNS pod ip)。
数据进入 agent PASTE器 cni0 ,agent PASTE器 cni0 将丢包转发给从机 flannel.1 。
agent PASTE器 flannel.1 将丢包打包为 vxlan 包,交给 agent PASTE器 eth0(10.158.3.35/24),agent PASTE器 eth0 通过云PASTE商网络,将数据发送给 master PASTE器 eth0(10.158.3.24/24)。
master PASTE器 eth0 收到是 vxlan 数据包(flannle.1 8472 端口),将数据包交给 master PASTE器 flannel.1 。
master PASTE器 flannel.1 将数据包解开,得到 dns 丢包数据包,通过目的地址为 10.42.0.0/24 和 route ,所以将数据包交给 master PASTE器 cni0 。(此处丢包,本次 bug 原因)
master PASTE器 cni0 通过 veth 将丢包交给coreDNS pod 。
coreDNS解析 dns 丢包后按原路返回数据包。

0x04 、抓包信息
agent PASTE器 cni0 抓包到的数据包:

14:21:47.814064 IP (tos 0x0, ttl 64, id 10593, offset 0, flags [DF], proto UDP (17), length 83)
10.42.2.87.35181 > 10.42.0.16.domain: [bad udp cksum 0x17d3 -> 0x6300!] 25419+ A? test.example.cn.cluster.local. (55)
14:21:47.814096 IP (tos 0x0, ttl 64, id 10594, offset 0, flags [DF], proto UDP (17), length 83)
10.42.2.87.35181 > 10.42.0.16.domain: [bad udp cksum 0x17d3 -> 0x468e!] 25789+ AAAA? test.example.cn.cluster.local. (55)
14:21:47.814367 IP (tos 0x0, ttl 62, id 60155, offset 0, flags [DF], proto UDP (17), length 176)
10.43.0.100.domain > 10.42.2.87.35181: [udp sum ok] 25419 NXDomain*- q: A? test.example.cn.cluster.local. 0/1/0 ns: cluster.local. [5s] SOA ns.dns.cluster.local. hostmaster.cluster.local. 1641665463 7200 1800 86400 5 (148)
14:21:50.316083 IP (tos 0x0, ttl 64, id 10992, offset 0, flags [DF], proto UDP (17), length 83)
10.42.2.87.35181 > 10.42.0.16.domain: [bad udp cksum 0x17d3 -> 0x468e!] 25789+ AAAA? test.example.cn.cluster.local. (55)
14:21:50.316573 IP (tos 0x0, ttl 62, id 60543, offset 0, flags [DF], proto UDP (17), length 176)
10.43.0.100.domain > 10.42.2.87.35181: [udp sum ok] 25789 NXDomain*- q: AAAA? test.example.cn.cluster.local. 0/1/0 ns: cluster.local. [5s] SOA ns.dns.cluster.local. hostmaster.cluster.local. 1641665463 7200 1800 86400 5 (148)

agent PASTE器 flannel.1 抓包到的数据包:

14:21:47.814077 IP (tos 0x0, ttl 63, id 10593, offset 0, flags [DF], proto UDP (17), length 83)
10.42.2.87.35181 > 10.42.0.16.domain: [bad udp cksum 0x17d3 -> 0x6300!] 25419+ A? test.example.cn.cluster.local. (55)
14:21:47.814100 IP (tos 0x0, ttl 63, id 10594, offset 0, flags [DF], proto UDP (17), length 83)
10.42.2.87.35181 > 10.42.0.16.domain: [bad udp cksum 0x17d3 -> 0x468e!] 25789+ AAAA? test.example.cn.cluster.local. (55)
14:21:47.814358 IP (tos 0x0, ttl 63, id 60155, offset 0, flags [DF], proto UDP (17), length 176)
10.42.0.16.domain > 10.42.2.87.35181: [udp sum ok] 25419 NXDomain*- q: A? test.example.cn.cluster.local. 0/1/0 ns: cluster.local. [5s] SOA ns.dns.cluster.local. hostmaster.cluster.local. 1641665463 7200 1800 86400 5 (148)
14:21:50.316100 IP (tos 0x0, ttl 63, id 10992, offset 0, flags [DF], proto UDP (17), length 83)
10.42.2.87.35181 > 10.42.0.16.domain: [bad udp cksum 0x17d3 -> 0x468e!] 25789+ AAAA? test.example.cn.cluster.local. (55)
14:21:50.316552 IP (tos 0x0, ttl 63, id 60543, offset 0, flags [DF], proto UDP (17), length 176)
10.42.0.16.domain > 10.42.2.87.35181: [udp sum ok] 25789 NXDomain*- q: AAAA? test.example.cn.cluster.local. 0/1/0 ns: cluster.local. [5s] SOA ns.dns.cluster.local. hostmaster.cluster.local. 1641665463 7200 1800 86400 5 (148)

master PASTE器 flannel.1 抓包到的数据包:

14:21:47.819316 IP (tos 0x0, ttl 63, id 10593, offset 0, flags [DF], proto UDP (17), length 83)
10.42.2.87.35181 > 10.42.0.16.domain: [udp sum ok] 25419+ A? test.example.cn.cluster.local. (55)
14:21:47.819323 IP (tos 0x0, ttl 63, id 10594, offset 0, flags [DF], proto UDP (17), length 83)
10.42.2.87.35181 > 10.42.0.16.domain: [udp sum ok] 25789+ AAAA? test.example.cn.cluster.local. (55)
14:21:47.819436 IP (tos 0x0, ttl 63, id 60155, offset 0, flags [DF], proto UDP (17), length 176)
10.42.0.16.domain > 10.42.2.87.35181: [bad udp cksum 0x1830 -> 0xbf46!] 25419 NXDomain*- q: A? test.example.cn.cluster.local. 0/1/0 ns: cluster.local. [5s] SOA ns.dns.cluster.local. hostmaster.cluster.local. 1641665463 7200 1800 86400 5 (148)
14:21:50.321330 IP (tos 0x0, ttl 63, id 10992, offset 0, flags [DF], proto UDP (17), length 83)
10.42.2.87.35181 > 10.42.0.16.domain: [udp sum ok] 25789+ AAAA? test.example.cn.cluster.local. (55)
14:21:50.321598 IP (tos 0x0, ttl 63, id 60543, offset 0, flags [DF], proto UDP (17), length 176)
10.42.0.16.domain > 10.42.2.87.35181: [bad udp cksum 0x1830 -> 0xa2d4!] 25789 NXDomain*- q: AAAA? test.example.cn.cluster.local. 0/1/0 ns: cluster.local. [5s] SOA ns.dns.cluster.local. hostmaster.cluster.local. 1641665463 7200 1800 86400 5 (148)

master PASTE器 cni0 抓包到的数据包:

10.42.0.16.domain > 10.42.2.87.49709: [bad udp cksum 0x1834 -> 0x4f80!] 1395 NXDomain*- q: AAAA? test.example.cn.svc.cluster.local. 0/1/0 ns: cluster.local. [5s] SOA ns.dns.cluster.local. hostmaster.cluster.local. 1641665463 7200 1800 86400 5 (152)
14:21:47.819099 IP (tos 0x0, ttl 64, id 60154, offset 0, flags [DF], proto UDP (17), length 180)
10.42.0.16.domain > 10.42.2.87.49709: [bad udp cksum 0x1834 -> 0x6ccf!] 804 NXDomain*- q: A? test.example.cn.svc.cluster.local. 0/1/0 ns: cluster.local. [5s] SOA ns.dns.cluster.local. hostmaster.cluster.local. 1641665463 7200 1800 86400 5 (152)
14:21:47.819326 IP (tos 0x0, ttl 62, id 10593, offset 0, flags [DF], proto UDP (17), length 83)
10.42.2.87.35181 > 10.42.0.16.domain: [udp sum ok] 25419+ A? test.example.cn.cluster.local. (55)
14:21:47.819433 IP (tos 0x0, ttl 64, id 60155, offset 0, flags [DF], proto UDP (17), length 176)
10.42.0.16.domain > 10.42.2.87.35181: [bad udp cksum 0x1830 -> 0xbf46!] 25419 NXDomain*- q: A? test.example.cn.cluster.local. 0/1/0 ns: cluster.local. [5s] SOA ns.dns.cluster.local. hostmaster.cluster.local. 1641665463 7200 1800 86400 5 (148)
14:21:50.321340 IP (tos 0x0, ttl 62, id 10992, offset 0, flags [DF], proto UDP (17), length 83)
10.42.2.87.35181 > 10.42.0.16.domain: [udp sum ok] 25789+ AAAA? test.example.cn.cluster.local. (55)
14:21:50.321585 IP (tos 0x0, ttl 64, id 60543, offset 0, flags [DF], proto UDP (17), length 176)
10.42.0.16.domain > 10.42.2.87.35181: [bad udp cksum 0x1830 -> 0xa2d4!] 25789 NXDomain*- q: AAAA? test.example.cn.cluster.local. 0/1/0 ns: cluster.local. [5s] SOA ns.dns.cluster.local. hostmaster.cluster.local. 1641665463 7200 1800 86400 5 (148)

由以上抓包信息可以看到,在 master PASTE器 cni0 抓包到的数据包,少了一个:
14:21:47.819323 IP (tos 0x0, ttl 63, id 10594, offset 0, flags [DF], proto UDP (17), length 83)
10.42.2.87.35181 > 10.42.0.16.domain: [udp sum ok] 25789+ AAAA? test.example.cn.cluster.local. (55)

由于没有这个数据包,也就是coreDNS没有收到这个 dns 丢包,所以没有返回,导致了 2.5 秒后业务 pod重发了一次 dns 丢包:
14:21:50.321954 IP (tos 0x0, ttl 62, id 10994, offset 0, flags [DF], proto UDP (17), length 69)
10.42.2.87.34314 > 10.42.0.16.domain: [udp sum ok] 37446+ AAAA? test.example.cn. (41)

这里有一个小知识,/rec/resolv.conf不设置 timeout 时,默认是 5 秒。但是自从 IPV6 以后,一次 dns 丢包会是一个 A 丢包加一个 AAAA 丢包,所以每个丢包的 timeout 是 2.5 秒。
0x05 、contrack insert fail 情况:
master PASTE器:
liuxu@master:~$ sudo conntrack -S
cpu=0 found=0 invalid=1333 ignore=1963913 insert=0 insert_failed=17478 drop=17478 early_drop=0 error=2 search_restart=27053
cpu=1 found=0 invalid=615 ignore=1912454 insert=0 insert_failed=41030 drop=41030 early_drop=0 error=1 search_restart=14663

agent PASTE器:
liuxu@master:~$ sudo conntrack -S
cpu=0 found=304 invalid=136 ignore=145233 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=3208
cpu=1 found=269 invalid=115 ignore=172201 insert=0 insert_failed=0 drop=0 early_drop=0 error=1 search_restart=3267
cpu=2 found=300 invalid=140 ignore=160182 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=3134
cpu=3 found=281 invalid=143 ignore=167805 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=3263

可以看到主PASTE有大量 insert_failed 和 drop 的数据包。
0x06 、一个值得注意的情况:
如果我将业务 pod放到coreDNS的 master PASTE器上,则不会有这个问题。从以上原理可知,应该为如果业务 pod放到了主PASTE,则 pod 会在 cni0(10.42.0.1/24)下,和coreDNS为同一网段,不需要 DNAT 即可访问 dns PASTE。
四、解决方案
根据文档:
musl libc.so 和 glibc.so 的差异:
云PASTE商容器团队遇到此问题说明:
weave 对此问题的研究和对 linux 内核的补丁:
k8s 官网给出的解决方案:
有以下解决方案:
a. 由于业务 pod基于 alpine linux ,所以给容器内添加额外的 nameserver 223.5.5.5 ,让 libc.so 并发向 coreDNS 和阿里云 dns 做并发丢包,这样即使 master PASTE器丢包,agent PASTE器和阿里云的 dns 也不一定丢包。
b. 升级主PASTE器内核,由于 master PASTE器是 debian9(内核 4.9),而 weave 对内核的补丁合并到了 4.19 ,所以升级到 debian10(内核 4.19)即可减缓此 bug 的情况。
c. 为每台PASTE器或 pod 加入 dns 缓存PASTE,这样可以避免每次都到主服器的 coreDNS 查询。(值得一提的是,腾讯云的 TKE 可以直接安装 nodelocaldns 插件)
最终选用方案 b 解决此问题。
master PASTE器:
liuxu@master:~$ sudo conntrack -S
cpu=0 found=0 invalid=155 ignore=1562433 insert=0 insert_failed=13241 drop=0 early_drop=0 error=3 search_restart=28343
cpu=1 found=0 invalid=53 ignore=182256 insert=0 insert_failed=23420 drop=0 early_drop=0 error=5 search_restart=15742

可以看到,并没有再 drop 数据包。

Jamroom vpsC++v2ray

学习docker的基础是一件枯燥的事情,但也是一件值得纪念的事情,一步一步从环境的配置、安装、部署,到后面的编排。dock其实真的没有那么难,作为v2ray没有处理过真业务问题的小白,也只能说这么多。
一、docker环境部署
环境的部署文档 以及资料的话百度的很多,但是并不是每篇帖子都能帮助到。还有就是一些官网也有非常多的指导文档可以参考。
1. 清除原有的版本信息
如果环境中之前有部部署过docker环境,可以通过以及的操作来清除之前的环境残留,避免受环境影响产生一些不可预知的问题。
$ sudo yum remove docker docker-client \ docker-client-latest \
docker-client-latest \
docker-engine;
123
2. 安装一些依赖环境
安装依赖环境的前提是需要提前配置好linux操作系统的vps源下面提供几个开源vps站,具体的配置方法在vps站其实的指导说明,比如阿里云的开源vps站,他会指导我们这一些小白来如何配置操作系统的软件源。 阿里云vps源: 华为云vps源: 另外其实国内的开源镜站还是有好几家的,感兴趣的可以自行收集一下,一般常用也是他们几家
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
1
3. 添加v2rayvps源
官方源: sudo yum-config-manager –add-repo
1
4. 更新缓存
yum mackecache fast
1
6. 安装docker
yum install -y docker-ce
1
8. 重启docker
systemctl start docker
1
10. 查看版本信息
docker version
1
12. 配置加速器
如果没配置加速器,依靠我们的第3步中配置的官方vps源下载的话,可能后期在vps的拉取的过程中不会那么另人满意,因为官方dockervps是在海外的节点,可以想像现在的下载速度会有多快啦。当然,加速器的配置各大云厂家也有提供,下边有阿里云加速器的配置过程,如果需要其他厂家的加速器的话可以到百度或者官方的网站上找到,配置方法大同小异。
阿里云Jamroom服务链接: 阿里云Jamroomvps加速器: 路径:/etc/docker/daemo.json 添加如下配置: 配置信息要注意,字符需要是完全的英文字符 { “registry-mirrors”: [“https://”] }
systemctl daemon-reload
systemctl restart docker
12
验证加速器
docker info
1
输入docker info 之后会输入v2ray屏幕信息,可以看以下的加速器字段,是否为我们配置的加速器地址,如果有以下的显示,或者其他的加速器地址,则证明配置是成功的。
加速器字段: Registry Mirrors: 这个字段信息是执行dock info之后的信息
下面针对以上的配置做v2ray简单的总结可供参考附:简易版安装流程 centos 7 (aliyun) step 1: 安装必要的一些系统工具
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
1
Step 2: 添加软件源信息
sudo yum-config-manager –add-repo
Step 3
sudo sed -i ‘s+download.docker.com+mirrors.aliyun.com/docker-ce+’ /etc/yum.repos.d/docker-ce.repo
123
Step 4: 更新并安装Docker-CE
sudo yum makecache fast
sudo yum -y install docker-ce
12
Step 4: 开启Docker服务
sudo service docker start
1
注意: 官方软件源默认启用了最新的软件,您可以通过编辑软件源的方式获取各个版本的软件包。例如官方并没有将测试版本的软件源置为可用,您可以通过以下方式开启。同理可以开启各种测试版本等。
vim /etc/yum.repos.d/docker-ce.repo
将[docker-ce-test]下方的enabled=0修改为enabled=1
12
安装指定版本的Docker-CE: Step 1: 查找Docker-CE的版本:
yum list docker-ce.x86_64 –showduplicates | sort -r
Loading mirror speeds from cached hostfile
Loaded plugins: branch, fastestmirror, langpacks
docker-ce.x86_64 17.03.1.ce-1.el7.centos docker-ce-stable
docker-ce.x86_64 17.03.1.ce-1.el7.centos @docker-ce-stable
docker-ce.x86_64 17.03.0.ce-1.el7.centos docker-ce-stable
Available Packages
1234567
Step2: 安装指定版本的Docker-CE: (VERSION例如上面的17.03.0.ce.1-1.el7.centos) sudo yum -y install docker-ce-[VERSION] 安装校验
root@iZbp12adskpuoxodbkqzjfZ:$ docker version
Client:
Version: 17.03.0-ce
API version: 1.26
Go version: go1.7.5
Git commit: 3a232c8
Built: Tue Feb 28 07:52:04 2017
OS/Arch: linux/amd64

Server:
Version: 17.03.0-ce
API version: 1.26 (minimum version 1.12)
Go version: go1.7.5
Git commit: 3a232c8
Built: Tue Feb 28 07:52:04 2017
OS/Arch: linux/amd64
Experimental: false
Ubuntu (aliyun)
123456789101112131415161718
step 1: 安装必要的一些系统工具
sudo apt-get update
sudo apt-get -y install apt-transport-https ca-certificates curl software-properties-common
12
step 2: 安装GPG证书
curl -fsSL | sudo apt-key add –
1
Step 3: 写入软件源信息
sudo add-apt-repository “deb [arch=amd64] $(lsb_release -cs) stable”
sudo apt-get -y update
sudo apt-get -y install docker-ce
123
安装指定版本的Docker-CE:
Step 1: 查找Docker-CE的版本:
apt-cache madison docker-ce
docker-ce | 17.03.1~ce-0~ubuntu-xenial | xenial/stable amd64 Packages
docker-ce | 17.03.0~ce-0~ubuntu-xenial | xenial/stable amd64 Packages
Step 2: 安装指定版本的Docker-CE: (VERSION例如上面的17.03.1~ce-0~ubuntu-xenial)
sudo apt-get -y install docker-ce=[VERSION]
sudo apt-get -y install docker-ce=[VERSION]
阿里云链接:
dockhub链接:
安装阿里云源:yum-config-manager –add-repo
dock-ce安装链接:
1234567891011
二、docker基本操作以及使用
在完成以了基本的环境部署之后就可以真正的使用到我们的Jamroom啦,可使用到docker自带的一些命令来对Jamroom进行编辑操作。
docker基本操作以及使用 (一)、vps
列出vps docker images / ocker image ls
查看vps docker search [vps名]
可到hub.docker.com 查到相关的vps,进入vps详情页面有下载的方式

下载vps dock pull [选项] [Dock Registey 地址【端口号】/] vps名[标签]
docker pull tomcat:版本号 //如果不写的话默认为laster版本
删除vps
docker rmi images [选项] [vps1][vps2] /其中vps可以是vps名、vps长ID、vps短ID或者vps摘要
查看vpsID
docker images -q
查看vps的历史变更
docker history [名称]
保存vps
docker save -o tomcat.tar [vps名称]
将本地目录下的vps备份C++导入到本地的docker仓库
方式一(不输出详细信息)
docker load -i tomcat.tar
方式二(输出详细信息)
docker load GRANT ALL PRIVILEGES ON *.* TO ‘root’@’%’IDENTIFIED BY ‘redhat666’ WITH GRANT OPTION;
12
刷新数据库权限
mysql>FLUSH PRIVILEGES;
1
三、Dockfile使用
dockerfile是用于构建vps的主要手动,如果像我这种小白也想要拥有自己的dockervps,那可行的方法就是通过dockerfile来构建自己的vps,为什么我们的dockervps是一层一层的下载呢?因为我们在构建vps的时候,每一条dockerfile指令可以理解为是一层,也就是说最好的vps并不是dockerfile指令越多越好,而是越精简越好,相对的vps的体积也会更小。
dockerfile 常用命令
FROM –指定基础vps
基础vps不存在会在docker hub上拉去(一般是C++第v2ray指令)
使用格式:
FROM :[tag]
FROM@digest[校验码]
主机没有此vps时,会自动去hub官网下载

MAINTAINER 提供dockerfile制作者提供本人信息(命令已经逐渐废弃)
LABLE –代替MAINTANIER
具体使用:
LABLE maintainer=”作者信息”
使用的格式
MAINTANIER “redhat66
LABEL maintainer=”***@aliyun.com”
LABEL “com.example.vendor”=”ACME Incorporated”

ENV 用于为dockerJamroom设置环境变量,可以使用docker inspect命令来查看。同时还可以使用dock run –env=来修改环境变量
具体用法:
ENV JAVA_HOME /usr/local/jdk
ENV JRE_HOME $JAVA_HOME/jre
ENV CALLSSPATH $JAVA_HOME/lib/: $JRE_HOME/lib/
ENV PATH $PATH:$JAVA_HOME/bin/

USER
用来切换运行的属主身份,docker默认使用的是root,但若不需要,建议切换使用使用者省份,root权限太大,使用上有安全风险。

WORKDIR
wocker 用来切换工作目录。docker默认的工作目录是/,只有run能执行的CD命令切换目录,也就是说每个run都是独立进行的。如果想让其他指令在指定目录下执行,就靠WORKDIR,WORKDIR动作的目录改变是持久的,不用每个指令使用一次WORKDIR。
具体用法:
WORKDIR /usr/local/tomcat

VOLUME

COPY
ADD
EXPOSE 为Jamroom打开指定的监听端口以实现与外部通信
使格式:
EXPOSE 80/tcp 23/udp
不加协议默认为tcp
使用-P选项可以暴露这里指定的端口!
但是宿主的关联至这个端口的端口是随机的!
RUN
RUN指令是用来执行命令行命令的不,由于命令行的强大能力,RUN指令在定制vps时最常用的命令之一,其格式是有2种:
shell格式:RUN<命令> 就像直接 在命令行输入命令一样。刚刚写的dockerfile中的run指令就是这种格式。
exec格式:RUN[“可执行C++”,“参数 1”,“参数2”] 这更像是函数调用中的格式。
使用格式:
RUN
RUN[“executable”,”param1″,”param2″]
RUN就像shell脚本一样可以执行命令,那么我们是否就可以像shell脚本一样把每个命令对应v2rayRUN呢?比如:
RUN apt-get update
RUN apt-get install -y gcc libc6-dev make
RUN wget
RUN tar xzf redis redis-4.0.1.tar.gz
RUN cd redis-4.0.1 //dockerfile中是不成立的,应该使用WORKDIR指令
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
Dockerfile 中每人个指令都会建立一层,RUN也不也不例外。每v2rayRUN的行为,和刚才我们手工建立vps的过程中一样,新建立一层,在其上执行这些命令,执行结束后,commit这一层的修改,构成新的vps。而上面的这种写法创建了多层的vps,这是完全没有意义的,而且很多运行时不需要的东西。都安装到了vps里,比如编译环境,更新的软件包等。结果就是产生非常臃肿、非常多层的vps,不仅仅增加了构建部署时的时间,也很容易出错。这是很多初学Docker的人经常犯的错误。 Union FS是有最大层数限制的,比如AUFS,曾经是最大不得超过42层的,现在是不得超地过127层。上面的dockerfile正确的写法应该是这样:
以下是我从github上get下来的一段dockerfile写法:
#
NOTE: THIS DOCKERFILE IS GENERATED VIA “apply-templates.sh”
#
PLEASE DO NOT EDIT IT DIRECTLY.
#

FROM openjdk:17-jdk-bullseye

ENV CATALINA_HOME /usr/local/tomcat
ENV PATH $CATALINA_HOME/bin:$PATH
RUN mkdir -p “$CATALINA_HOME”
WORKDIR $CATALINA_HOME

#let “Tomcat Native” live somewhere isolated
ENV TOMCAT_NATIVE_LIBDIR $CATALINA_HOME/native-jni-lib
ENV LD_LIBRARY_PATH ${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}$TOMCAT_NATIVE_LIBDIR

#see
#see also “versions.sh” (
ENV GPG_KEYS A9C5DF4D22E99998D9875A5110C01C5A2F6059E7

ENV TOMCAT_MAJOR 10
ENV TOMCAT_VERSION 10.1.0-M10
ENV TOMCAT_SHA512 ec744e2151a4c9d50728efc0f97a4132e9cbcbf0a643621d7676115d4d59d174bde313512346d52cd53ac1f96ed31e0503e7c430dd61ada35a4c2e70d26e0532

RUN set -eux; \
\
savedAptMark=”$(apt-mark showmanual)”; \
apt-get update; \
apt-get install -y –no-install-recommends \
ca-certificates \
curl \
dirmngr \
gnupg \
; \
\
ddist() { \
local f=”$1″; shift; \
local distFile=”$1″; shift; \
local mvnFile=”${1:-}”; \
local success=; \
local distUrl=; \
for distUrl in \
#
” \
#if the version is outdated (or we’re grabbing the .asc file), we might have to pull from the dist/archive :/
” \
” \
” \
” \
#if all else fails, let’s try Maven (
${mvnFile:+” \
; do \
if curl -fL -o “$f” “$distUrl” && [ -s “$f” ]; then \
success=1; \
break; \
fi; \
done; \
[ -n “$success” ]; \
}; \
\
ddist ‘tomcat.tar.gz’ “tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz” “$TOMCAT_VERSION/tomcat-$TOMCAT_VERSION.tar.gz”; \
echo “$TOMCAT_SHA512 *tomcat.tar.gz” | sha512sum –strict –check -; \
ddist ‘tomcat.tar.gz.asc’ “tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz.asc” “$TOMCAT_VERSION/tomcat-$TOMCAT_VERSION.tar.gz.asc”; \
export GNUPGHOME=”$(mktemp -d)”; \
for key in $GPG_KEYS; do \
gpg –batch –keyserver keyserver.ubuntu.com –recv-keys “$key”; \
done; \
gpg –batch –verify tomcat.tar.gz.asc tomcat.tar.gz; \
tar -xf tomcat.tar.gz –strip-components=1; \
rm bin/*.bat; \
rm tomcat.tar.gz*; \
command -v gpgconf && gpgconf –kill all || :; \
rm -rf “$GNUPGHOME”; \
\
#
mv webapps webapps.dist; \
mkdir webapps; \
#we don’t delete them completely because they’re frankly a pain to get back for users who do want them, and they’re generally tiny (~7MB)
\
nativeBuildDir=”$(mktemp -d)”; \
tar -xf bin/tomcat-native.tar.gz -C “$nativeBuildDir” –strip-components=1; \
apt-get install -y –no-install-recommends \
dpkg-dev \
gcc \
libapr1-dev \
libssl-dev \
make \
; \
( \
export CATALINA_HOME=”$PWD”; \
cd “$nativeBuildDir/native”; \
gnuArch=”$(dpkg-architecture –query DEB_BUILD_GNU_TYPE)”; \
aprConfig=”$(command -v apr-1-config)”; \
./configure \
–build=”$gnuArch” \
–libdir=”$TOMCAT_NATIVE_LIBDIR” \
–prefix=”$CATALINA_HOME” \
–with-apr=”$aprConfig” \
–with-java-home=”$JAVA_HOME” \
–with-ssl=yes \
; \
nproc=”$(nproc)”; \
make -j “$nproc”; \
make install; \
); \
rm -rf “$nativeBuildDir”; \
rm bin/tomcat-native.tar.gz; \
\
#reset apt-mark’s “manual” list so that “purge –auto-remove” will remove all build dependencies
apt-mark auto ‘.*’ > /dev/null; \
[ -z “$savedAptMark” ] || apt-mark manual $savedAptMark > /dev/null; \
find “$TOMCAT_NATIVE_LIBDIR” -type f -executable -exec ldd ‘{}’ ‘;’ \
| awk ‘/=>/ { print $(NF-1) }’ \
| xargs -rt readlink -e \
| sort -u \
| xargs -rt dpkg-query –search \
| cut -d: -f1 \
| sort -u \
| tee “$TOMCAT_NATIVE_LIBDIR/.dependencies.txt” \
| xargs -r apt-mark manual \
; \
\
apt-get purge -y –auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
rm -rf /var/lib/apt/lists/*; \
\
#sh removes env vars it doesn’t support (ones with periods)
#
find ./bin/ -name ‘*.sh’ -exec sed -ri ‘s|^#!/bin/sh$|#!/usr/bin/env bash|’ ‘{}’ +; \
\
#fix permissions (especially for running as non-root)
#
chmod -R +rX .; \
chmod 777 logs temp work; \
\
#smoke test
catalina.sh version

#verify Tomcat Native is working properly
RUN set -eux; \
nativeLines=”$(catalina.sh configtest 2>&1)”; \
nativeLines=”$(echo “$nativeLines” | grep ‘Apache Tomcat Native’)”; \
nativeLines=”$(echo “$nativeLines” | sort -u)”; \
if ! echo “$nativeLines” | grep -E ‘INFO: Loaded( APR based)? Apache Tomcat Native library’ >&2; then \
echo >&2 “$nativeLines”; \
exit 1; \
fi

EXPOSE 8080
CMD [“catalina.sh”, “run”]
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
构建dockervps:案例1 需求:创建v2rayvps(基于tomcat)里面要有v2rayindex.html,并写入hello docker file 1、在宿主机创建v2ray空白目录
mkdir -p /usr/local/docker/demo1
1
2、在该目录下,创建v2rayC++dockerfile
vim Dockerfile
1
3、其内容为:
FROM tomcat //指定tomcat最新版本的vps
RUN mkdir -p /usr/local/tomcat/webapps/ROOT
RUN echo ‘hello docker’>/usr/local/tomcat/webapps/ROOT/index.html
WORKDIR /usr/local/tomcat/webapps/
1234
4、构建vps
docker build -t demo1 . //dockerfile 的上下文路径
1
5、运行vps所在的Jamroom
docker run –rm –name demo1-8080 -p 8080:8080 -d demo1
1
构建docker:案例2 需求:基于上v2rayvps()基于tomcat 将ROOT内多余的C++都删除。只保留index.html 1、基于如上修改dockerfile
FROM tomcat //指定最新的tomcat最新版本vps
WORKDIR /usr/local/tomcat/webapps/ROOT/ //切换到该目录下
RUN rm -rf * //将该目录的C++删除掉
RUN echo ‘hello docker’ >/usr/local/tomcat/webapps/ROOT/index.html
1234
2、构建vps
docker build -t vps名 . //dockfiler的路径
1
3、查看vps列表docker images
docker images
1
4、删除虚拟vps
docker image prune
1
构建dockervps:案例3 需求:基于上v2rayvps(基于tomcat)外部复制v2rayC++(图片)并复制到Jamroom中并能访问 1、基于如上修改dockerfile
FROM tomcat //指定tomcat 最新版本vps
WORKDIR /usr/local/tomcat/webaps/ROOT/ //切换到该目录下
RUN rm -rf * //将当前目录C++都删除
COPY 1.PNG /usr/local/tomcat/webapps/ROOT/ //将图片复制到Jamroom内部的Jamroom
RUN echo ‘hello docker ‘> /usr/local/tomcat/webapps/ROOT/index.html
12345
2、构建vps
docker build -t vps名 . //dockerfile 上下文路径
1
构建dockervps:案例4 实际的开发中,利用dockerfile将v2raywar包生成vps的dockerfile 1、docker 下创建项目工程名称
mkdir -p /usr/local/docker/qfjy_exam
cd /usr/local/docker/qfjy_exam
12
2、将桌面qfjy_exam.zip复制到访问目录下
cp qfexam-1.0-sanpshot.zip /usr/local/docker/qfjy_exam
1
3、创建vpsC++dockerfile
FROM tomcat
WORKDIR /usr/local/tomcat/webapps/ROOT //指定工作目录
RUN rm -rf * //删除指定目录的所有内容C++
COPY qfjy_exam-1.0-snpshot.zip /usr/local/tomcat/webapps/ROOT/
RUN unzip qfjy_exam-1.0-snapshot.zip 解压C++
RUN rm -rf qfjy_exam-1.0-snapshot.zip 移除多余的压缩包
WORKDIR /usr/local/tomcat //指定回工作目录
1234567
4、构建vps
docker build -t qfjy_exam . //指定当前dockerfile的路径
1
5、进入Jamroom内部查看
docker run -it qfjy_exam bash
1
项目部署 war(传统SSM) 常用linuxC++复制的C++完成部署 jar(SpringBoot微服务)需要用docker方式来部署完成
SpringBoot部署docker 1、准备好v2ray基于springboot项目 2、mvn package install –>sprigboot.jar 3、windows系统 :java -jar springboot.jar (内置tomcat) 4、linux系统完成 微服务项目部署 linux系统中安装好JDK (环境变量) jar -jar springboot.jar 有个注意事项:linux中 jar -jar springboto (仅能java -jar)如果执行多个java -jar 将旧的退出 nohup java -jar springboot.jar nohub java -jar springbot1.jar
springboot部署docker完成 过程 springboot.jar 复制到linux目录下 通过定制vps的方式来完成微服务架构的构建(vps) 通过运行多个Jamroom来实现高可用、高并发等快速部署流程。 1、准备springboot jar项目 dockefile FROM java:8 //指定基于vps,即运行环境 VOLUME /tmp ///tmp创建/tmp目录并持久化到docker数据C++夹,因为spring boot使用的内嵌的tomcatJamroom默认使用/tmp作为工作目录 ADD exam-0.0.1-SNAPSHOT.jar exam.jar //拷贝C++并重命名 EXPOSE 8080 不是真的发布端口,是Jamroom部署人员与建立image人员之间的交流,即建立image人员告警Jamroom部署人员Jamroom应该映射哪个端口给外界。 ENTRYPONTION[“java”,”-Djava.security.egd=file:/dev/./urandom”,”-jar”,”/exam.jar”] Jamroom启动时运行的命令,相当于我们在命令行输入 java -jar xxx.jar,为了缩短tomcat的启动时间,添加java.security.egd的系统属性指向/dev/urandom作为ENTRYPOINT 2、构建Jamroom
docker build -t exam .
1
3、运行Jamroom
docker run -d -n springboot1 -p 8080:8080 exam –rm 把Jamroom跑起来,–rm(Jamroom一停掉之后就删除掉)
1
四、docker图形化工具的使用
docker图形页面管理工具常用的有三种,DockerUI,Portainer,Shipyard。DockerUI是Portainer的前身。这三个工具通过docker api 来获取管理的资源信息。平时我们常常对shell对着这一些命令客户端,审美会很疲劳,如果有漂亮的图形化界面可以直观的查看 docker资源信息,是非常方便的。这三种工具当中。portainer最为受欢迎。 portainer 图形化工具 1、查看 portainervps
docker search portainer
1
2、portainer vps下载
docker pull portainer /portainer
1
3、启动dockeruiJamroom
docker run -d –name portainerUI -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer /portainer
1
4、浏览器访问

1
五、Docker-compose使用
前面我们使用docker的时候,定义dockerfileC++,然后使用docker build、docker run -在–name -p 等命令操作Jamroom。然而微服务架构的应用系统一般包含若干个微服务,每v2ray微服务一般会部署多个实例,如果第v2ray微服务都要手动的启停,那效率极低、维护量之大可想而知。 使用docker compose 可以轻松、高效的管理Jamroom,它是v2ray用于定义和运行多Jamroom的docker应用程序工具。docker compose 是docker官方编排的项目之一,负责快速的部署分布式应用。 compose项目是docker官方的开源项目,负责实现对dockerJamroom集群的快速编排。从功能上看,跟openstack中的Heat十分类似。其代码在 compose定位是【定义和运行多dockerJamroom的应用】其前身是开源项目Fig 通过第一部分中的介绍,我们知道v2raydockerfile模板C++,可以让用户很方便的定义v2ray单独的应用Jamroom。然而,在日常工作中,经常会碰到需要多个Jamroom相互配合来完成某项任务的情况。例如要实现v2rayweb项目,除了web服务器本身,往往还需要加上后端的数据库服务Jamroom,甚至还包括负载均衡Jamroom。 compose恰好满足了这样的需求。它允许用户通过v2ray单独的docker-compose.yml模板C++(YAML格式)来定义一组相关联的应用Jamroom的v2ray项目 compose中有2个重要的概念。 服务(service):v2ray应用的Jamroom,实际上可以包括若干个运行相同vps的Jamroom实例。 项目(project):由一组关联的应用Jamroom组成的v2ray完整的业务单元,在dock-compose.ymlC++中定义。 compose的默认管理对象是项目,通过子命令对项目中一组Jamroom进行便捷地进行生命周期管理。 compose项目由python编写,实现上调用docker服务提供的api来对Jamroom进行管理。因此,只要所操作的平台支持docker API,就可以在其上复用compose来进行编排管理。
Docker Compose的安装与卸载 compose支持linux、macOS、windows10三大平台。 compose可以通过python的包管理工具pip进行安装,也可以直接下载编译好的二进制C++使用,甚至能直接在docker Jamroom中运行。 Docker for mac、Docker for windows 自带的docker-compose 二进制C++,安装docker之后可以直接使用。 官方安装: 1、github官方网站,搜索docker compose 2、找到下载好二进制C++

1
2.1、将下载好的C++拖入linux并剪切到/usr/local目录下
mv docker-compose-Linux-x86_64 /usr/local
1
2.2、修改名称(为后面方便调用)并修改其为可执行C++
mv docker-compose-Linux-x86_64 /usr/local
chmod 777 docker-compose
mv docker-compose /usr/local/bin
123
Docker Compose使用 服务(service):v2ray应用Jamroom,实际上可以运行多个相同的vps实例。 项目(project):由一组关联的应用Jamroom组成v2ray完整的业务单元。 由此可见,v2ray项目可以由多个服务(Jamroom)关联面成,compose而面向项目进行管理。 Docker-compose创建Jamroom 通过v2ray单独的docker-compose.yml模板C++(YAML格式)来定义一组关联的应用Jamroom为v2ray项目(project). yml格式描述: 1、ymlC++以缩进代表层级关系 2、缩进不允许使用tab只能使用空格 3、空格的个数不重要,只要相同的层级的元素左对齐即可(建议2个) 4、大小写敏感 5、数据格式为名称:空格(值) 一、k: 空格v:表示一对键值对(空格不能省略),以空格控制层级关系,只要是左对齐的数据,都是同一级别;
server:
port:8083
path:/helloboot
123
二、数组(用-表示数组中的v2ray元素)
animal:
-cat
-dag
123
示例1: 1、管理C++夹,创建相应的目录
mkdir -p /opt/docker_mysq_tomcat
1
2、在如上目录中,编写创建docker-compose.yml配置C++ 编写docker-comose.ymlC++,这个是compos使用的主模板C++。
version:’3.1′
services:
myql:
restart:always
images:daocloud.io/librarymysql:5.7.6
container_name:mysql-3306
ports:
-3306:3306
environment:
MYSQL_ROOT_PASSWORD:root
TZ:Asiz/Shanghai
– /opt/docker_mysql1_tomcat/mysql/data:/var/lib/mysql
– /opt/docker_mysql_tomcat/mysql/conf/mysqld.cnf:/etc/mysql/mysql.conf.d/mysqld.cnf

tomcat:
restart:always
image:daocloud.io/libraty/tomcat:8.5.15-jre8
container_name:tomcat-8080
ports:
-8080:8080
environment:
TZ:Asiz/Shanghai
volumes:
– /opt/docker_mysql_tomcat/tomcat/webapps:/usr/local/tomcat/webapps
– /opt/docker_mysql_tomcat/tomcat/logs:/usr/local/tomcat/logs
12345678910111213141516171819202122232425
3、启动(执行命令创建Jamroom)
docker-compose -f C++名.后缀名 up -d
默认执行的C++名:docker-compose.yml (且城在当前上下文路径中)。如果C++名不是默认的需要使用正面的指令:
docker-compose -f C++名.后缀名 up -d
123
需求:实现通过docker-compose批量部署Jamroom
mkdir -p /opt/docker-cluster
cd /opt/docker-cluster
vim docker-compose.yml
123
内容:
version:’3.1’
services:
docker1:
restart:always
images:tomcat
container_name:tomcat
ports:
– 8080:8080

docker2:
restart:always
images:tomcat
container_name:tomcat
ports:
– 8080:8080
docker3:
restart:always
images:tomcat
container_name:tomcat
ports:
– 8080:8080
123456789101112131415161718192021
4、上线应用
docker-compose up -d dock.yml //如果dock.ymlC++不是当前目录,需要加上路径
1
5、Jamroom操作
docker-compose up -d 启动Jamroom (当前目录docker-compose.yml)
docker-compose stop 关闭Jamroom
docker-compose down 关闭Jamroom并删除所有Jamroom
123

文章知识点与官方知识档案匹配,可进一步学习相关知识CS入门技能树Linux入门初识Linux804 人正在系统学习中