containerd启动容器报错

报错信息

**Failed to create pod sandbox: rpc error: code = Unknown desc = failed to create containerd task: failed to create shim task: OCI runtime create failed: unable to retrieve OCI runtime error (open /run/containerd/io.containerd.runtime.v2.task/k8s.io/ed17cbdc31099314dc8fd609d52b0dfbd6fdf772b78aa26fbc9149ab089c6807/log.json: no such file or directory): runc did not terminate successfully: exit status 127: unknown**

系统

Linux k8s-node01 5.18.15-1.el7.elrepo.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Jul 28 09:26:15 EDT 2022 x86_64 x86_64 x86_64 GNU/Linux

原因

centos7默认的libseccomp的版本为2.3.1,不满足containerd的需求

解决

# rpm -e libseccomp-2.3.1-4.el7.x86_64
错误:依赖检测失败:
        libseccomp.so.2()(64bit) 被 (已安裝) chrony-3.4-1.el7.x86_64 需要
# rpm -e libseccomp-2.3.1-4.el7.x86_64 --nodeps

#### 重启containerd
# systemctl restart containerd
#### 查看执行结果
# kubectl  get pod -A
NAMESPACE     NAME                                       READY   STATUS    RESTARTS       AGE
kube-system   calico-kube-controllers-56cdb7c587-k8grb   1/1     Running   1 (91s ago)    18m
kube-system   calico-node-42qs2                          1/1     Running   2 (56s ago)    18m
kube-system   calico-node-sk6sg                          1/1     Running   1 (110s ago)   18m
kube-system   calico-node-vtq9d                          1/1     Running   1 (85s ago)    18m
kube-system   calico-typha-6775694657-24mzh              1/1     Running   1 (110s ago)   18m
Posted in 小技巧 | Leave a comment

vmware虚拟机配置共享

环境:

  • OS:CentOS-7-x86_64-Minimal-2009.iso
  • 内核版本:kernel-ml-5.18.15-1.el7.elrepo.x86_64
  • 虚拟机平台:VMware® Workstation 16 Pro
  • 虚拟机平台版本:16.2.3 build-19376536
# yum search vm-tools
已加载插件:fastestmirror
Loading mirror speeds from cached hostfile
 * base: mirrors.tuna.tsinghua.edu.cn
 * elrepo: mirrors.tuna.tsinghua.edu.cn
 * extras: mirrors.tuna.tsinghua.edu.cn
 * updates: mirrors.tuna.tsinghua.edu.cn
elrepo                                                                                                            | 3.0 kB  00:00:00     
elrepo/primary_db                                                                                                 | 396 kB  00:00:00     
========================================================= N/S matched: vm-tools =========================================================
open-vm-tools.x86_64 : Open Virtual Machine Tools for virtual machines hosted on VMware
open-vm-tools-desktop.x86_64 : User experience components for Open Virtual Machine Tools
open-vm-tools-devel.x86_64 : Development libraries for Open Virtual Machine Tools
open-vm-tools-test.x86_64 : Test utilities for Open Virtual Machine Tools
qemu-kvm-tools.x86_64 : KVM debugging and diagnostics tools

  名称和简介匹配 only,使用“search all”试试。

# yum install open-vm-tools
# vmware-hgfsclient
sharedir
# mkdir /mnt/hgfs/
# vmhgfs-fuse .host:/sharedir /mnt/hgfs
# cp /etc/fstab /etc/fstab.bak
##### /etc/fstab 中添加一行
.host:/sharedir /mnt/hgfs fuse.vmhgfs-fuse allow_other,defaults 0 0

# reboot
Posted in 小技巧 | Leave a comment

SSH登录Vmware虚拟机Linux服务器慢问题

现象:

每次通过ssh登录机器都特别慢,大约30秒,甚至更长

环境:

  • OS:CentOS-7-x86_64-Minimal-2009.iso
  • 虚拟机平台:VMware® Workstation 16 Pro
  • 虚拟机平台版本:16.2.3 build-19376536

解决:

vim /etc/ssh/sshd_config
# 找到UseDNS yes,打开注释,并设置为no
UseDNS no
# 重启sshd_server
systemctl restart sshd
Posted in 小技巧 | Leave a comment

移动终端网络接入

1.1 终端接入网络

1.1.1 移动终端接入网络有如下的几种情况

  • 终端设备在属地,终端设备通过基站,接入属地的网络
  • 终端设备在国内漫游时,移动设备会漫游连接到当地的网络,联通和电信则需要漫游回属地的网络
  • 终端设备在国外漫游时,需要漫游回属地的网络

1.1.2 基站接入分析

移动终端通过基站接入移动运营商网络,终端与基站之间是数据链路层,不涉及网络层(IP)和传输层(TCP),终端设备的IP地址是由运营商分配的,在切换基站时一般不会引起IP地址的变化

有以下几种情况:

  • 当设备进行重启、飞行模式切换等时,设备会重新发起接入,这时IP地址会发生改变
  • 设备在同一区域内切换基站的过程中,如果没有发生断网情况下,即没有重新接入,IP地址是不会变化的
  • 设备在区域间切换基站,比如联通设备从北京到河北,接入由北京联通变成河北联通,IP地址会发生变化

终端设备切换基站一般情况下可在50ms~200ms完成,TCP是基于连接的协议,连接状态由状态机来维护,连接完毕后,双方都会处于established状态,它们之间的连接由各自的IP和TCP的端口唯一标识,即使这个连接没有任何数据,但仍是保持连接状态。TCP的KeepAlive机制用于检测连接死活,一般时间为 7200 s,失败后重试 10 次,每次超时时间 75 s,以释放无效链接。这个时间比切换基站时间要大的多,因此TCP通道在切换基站时,其IP地址一般没有变化,所以基于IP和端口的已建立的TCP连接不会失效。

1.1.3 DNS解析

当前移动 DNS 的现状:

  • 运营商 LocalDNS 出口根据权威 DNS 目标 IP 地址进行 NAT,或将解析请求转发到其他DNS 服务器,导致权威 DNS 无法正确识别运营商的 LocalDNS IP,引发域名解析错误、流量跨网。
  • 域名被劫持的后果:网站无法访问(无法连接服务器)、访问到钓鱼网站等。
  • 解析结果跨域、跨省、跨运营商、国家的后果:网站访问缓慢甚至无法访问。

为了解决这些问题,通常TCP网关的地址可以通过HttpDNS技术获取,以避免DNS解析异常、域名劫持的问题。客户端直接访问HTTPDNS接口,获取服务最优IP,返回给客户端,客户拿到IP地址后,直接使用此IP地址进行连接。

1.2 接入层

接入层最靠近客户端,接入层一般使用LVS(DR模式)+VIP+HAProxy来实现,如果使用公有云也可以使用云服务提供的负载均衡服务,如使用腾讯云的CLB,阿里云的ALB,配置按TCP转发;有矿的话可以使用F5硬件来做接入层;保留这一层有如下好处:

  • 负载均衡:均衡客户端连接,尽量保证连接在连接服务器上均衡
  • 真实服务不需要公网IP,因为它不需要对外暴露IP地址,更安全
  • 会话保持

1.3 长连接服务器

长连接服务部署的机器关注以下几个配置项

nf_conntrack_max
nf_conntrack_max 决定连接跟踪表的大小,当nf_conntrack模块被装置且服务器上连接超过这个设定的值时,系统会主动丢掉新连接包,直到连接小于此设置值才会恢复。
Backlognet.core.somaxconn排队等待接受的最大连接数

net.core.netdev_max_backlog数据包在发送给cpu之ueej被网卡缓冲的速率,增加可以提高有高带宽机器的性能
文件描述符sys.fs.file-max允许的最大文件描述符 /proc/sys/fs/file-max

nofile应用层面允许的最大文件描述数 /etc/security/limits.conf
portsnet.ipv4.ip_local_port_range端口范围

net.ipv4.tcp_tw_reuse端口复用,允许time wait的socket重新用于新的连接,默认为0,关闭短连接设置为1

net.ipv4.tcp_tw_recycletcp连接中的time wait的sockets快速回收,默认为0,表示关闭

1.4 服务实现

1.4.1 认证:

验证终端身份,确保只有合法的终端才能够使用服务,流程如下

  1. 服务端生成设备私钥、公钥;私钥执久化到设备上,公钥保存在服务端
  2. 握手:客户端发起TCP连接,TCP连接建立成功后,服务端生成256字符随机字串 randomMsg,返回客户端
  3. 客户端登录:客户端拿出token+randomMsg,使用其私钥签名得到 secretChap,并把token、secretChap 通过TCP通道上报服务端
  4. 服务端验证:服务端使用设备公钥验证签名,并调用 Passport 服务验证 token,拿到用户信息;服务端配置用户、设备的路由信息
  5. 协商对称密钥:服务端验证后,生成并返回对称密钥 secureKey,返回的对称密钥 secureKey 使用终端的公钥加密,只有使用设备的私钥才可以解密;客户端解密对称密钥 secureKey,至此服务端和终端完成密钥协商,之后可以愉快并安全的通信了。

模拟代码:

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.RandomStringUtils;

import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;

public class AuthClientTest {
    private static final byte[] PARAM_IV;

    static {
        String PARAM_IV_CONFIG = Base64.encodeBase64String(new SecureRandom().generateSeed(32));
        PARAM_IV = Base64.decodeBase64(PARAM_IV_CONFIG);
    }

    private static class ClientStore {
        private static final byte[] PARAM_IV = AuthClientTest.PARAM_IV;
        String privateKey;
        String token;// 用户标识信息

        String randomMsg;
        // 对称密钥
        byte[] secretKey;
    }

    private static class ServerStore {
        private static final String SECRET_KEY = RandomStringUtils.random(32);
        private static final byte[] PARAM_IV = AuthClientTest.PARAM_IV;

        // 对称密钥
        byte[] secretKey = new SecureRandom().generateSeed(32);

        String publicKey;
        String randomMsg;
    }

    public static void main(String[] args) throws Exception {
        // s0 初始化,生成设备的公私钥
        SHA256SignUtil.RsaKeys rsaKeys = SHA256SignUtil.generateKeyBytes();
        ClientStore clientStore = new ClientStore();
        ServerStore serverStore = new ServerStore();
        // 发送私钥到客户端
        clientStore.privateKey = Base64.encodeBase64String(rsaKeys.getPrivateKey());
        // 公钥保存在服务器端
        serverStore.publicKey = Base64.encodeBase64String(rsaKeys.getPublicKey());
        // s1: 握手
        // s1.1 客户端连接服务端 app -> server
        serverStore.randomMsg = RandomStringUtils.randomAlphanumeric(256);
        // s1.2 TCP建立成功后 server -(randomMsg)-> app
        clientStore.randomMsg = serverStore.randomMsg;
        // s2 端侧签名登录 app -(token,secretChap:privateKey签名)-> server
        clientStore.token = genToken(1000L);
        Map<String, Object> data = new HashMap<>();
        data.put("token", clientStore.token);
        PrivateKey privateKey = SHA256SignUtil.restorePrivateKey(Base64.decodeBase64(clientStore.privateKey));
        byte[] secretChap = SecretChapUtils.createSecretChap(data, clientStore.randomMsg, privateKey);
        // s3 服务端验证登录
        PublicKey publicKey = SHA256SignUtil.restorePublicKey(Base64.decodeBase64(serverStore.publicKey));
        boolean verify = SecretChapUtils.verifySecretChap(data, serverStore.randomMsg, secretChap, publicKey);
        System.out.println(verify);
        // s4 服务端下发对称密钥 server -(secretKey:publicKey加密)-> app
        byte[] secureKey = SHA256SignUtil.encryptByPublicKey(serverStore.secretKey, publicKey.getEncoded());
        // s5 端侧解密对称密钥并存储
        clientStore.secretKey = SHA256SignUtil.decryptByPrivateKey(secureKey, privateKey.getEncoded());
        // s6 正常加密传输数据
        byte[] encrypt = AesUtil.encrypt("田加国是好人".getBytes(StandardCharsets.UTF_8), clientStore.secretKey, ClientStore.PARAM_IV);
        byte[] decrypt = AesUtil.decrypt(encrypt, serverStore.secretKey, ServerStore.PARAM_IV);
        String sourceData = new String(decrypt, StandardCharsets.UTF_8);
        System.out.println(sourceData);
    }

    private static String genToken(long uid) {
        HashMap<String, Object> claims = new HashMap<>();
        claims.put("iss", "user.tianjiaguo.com");
        claims.put("expire", System.currentTimeMillis() + 24 * 60 * 60 * 1000L * 30);
        claims.put("uid", uid);
        claims.put("type", 2);
        return Jwts.builder().setClaims(claims)
                .signWith(SignatureAlgorithm.HS256, ServerStore.SECRET_KEY.getBytes())
                .compact();
    }
}

1.4.2 连接保持

心跳机制+自适应心跳:

  1. 端侧定时发送心跳包,服务端重置心跳检查点
  2. 端侧会根据业务数据,决定是否、何时上报心跳包
  3. 心跳包频率可控

附录:

示例架构图

未完

Posted in 未分类 | Leave a comment

博客网站搭建

最近把网站迁移到国内的云服务厂商上,之前几次迁移部署都是采用编译安装的方式,非常的不方便,此次采用的是docker方式搭建环境的配置,前端也不再采用nginx,采用了能够自动生成和替换证书的ca#ddy来实现的。

安装workpress

安装docker和docker-compose

# 安装docker
...
# 安装docker-compose
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

编写wordpress的docker-compose.yml文件

version: "3.9"
    
services:
  db:
    image: mysql:8.0.25
    volumes:
      - ~/docker/mysql/data:/var/lib/mysql
    # docker的重启策略:在容器退出时总是重启容器,但是不考虑在Docker守护进程启动时就已经停止了的容器
    restart: unless-stopped
    command:
      # MySQL8的密码验证方式默认是 caching_sha2_password,但是很多的连接工具还不支持该方式
      # 就需要手动设置下mysql的密码认证方式为以前的 mysql_native_password 方式
      --default-authentication-plugin=mysql_native_password
      --character-set-server=utf8mb4
      --collation-server=utf8mb4_general_ci
    environment:
      MYSQL_ROOT_PASSWORD: {数据库ROOT密码}
      MYSQL_DATABASE: {数据库名称}
      MYSQL_USER: {数据库普通用户名,可用来做wordpress的用户}
      MYSQL_PASSWORD: {数据库普通用户的密码}
      TZ: Asia/Shanghai
    ports:
      - 3306:3306
    volumes:
      - ~/docker/mysql/data:/var/lib/mysql
      - ~/docker/mysql/conf:/etc/mysql/conf.d
      - ~/docker/mysql/logs:/logs
    networks:
      - app-network
    
  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    ports:
      - "8000:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: {数据库名称}
      WORDPRESS_DB_USER: {数据库普通用户名,可用来做wordpress的用户}
      WORDPRESS_DB_PASSWORD: {数据库普通用户的密码}
    volumes:
      - ~/docker/wordpress:/var/www/html
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

本地需要创建mysql和wordpress的目录,docker会把本地创建的目录挂载到docker镜像中,这样mysql的数据和wordpress的配置才会在docker销毁时不会丢失。

mkdir mysql
mkdir mysql/conf
mkdir mysql/data
mkdir mysql/log
mkdir wordpress

启动docker镜像

docker-compose up

然后临时打云厂商的防火墙的8000端口,在本地看是docker是否能到安装配置页面,如果可以正常打开,说明wordpress已经安装配置完成,可以使用 docker-compose up -d 转到后台执行,我们现在需要把云厂商的防火墙关闭。

安装配置ca#ddy

编写执行脚本

#!/usr/bin/env sh

VERSION=2.4.1
IMAGE=caddy:${VERSION}
CMD=""

docker run -itd --restart=always \
  --name caddy \
  --net=host \
  -v $PWD/conf/Caddyfile:/etc/Caddyfile \
  -v $PWD/data:/data/caddy \
  -v $PWD/www:/opt/www \
  ${IMAGE} caddy run -config /etc/Caddyfile

编写Ca#ddy的配置文件

https://www.{域名}.com https://{域名}.com http://www.{域名}.com http://{域名}.com {

    tls internal {
        on_demand
    }

    route /* {
        reverse_proxy * localhost:8000 {
            header_up -Origin
        }
        encode * {
            gzip
            zstd
        }
    }

    log {
        output stdout
    }

}

预创建目录

mkdir caddy
mkdir caddy/conf
mkdir caddy/data
mkdir caddy/www

执行脚本启动服务器,服务启动后我们在本地机器上可以通过配置hosts的方式,连接上去,看是否能打开wordpress的安装页面。

配置证书

我们使用cloudflare的证书,需要先到其官方网站上注册 https://dash.cloudflare.com/login 账号。

接下来我们需要在网站上添加我们的网站

然后到自己的域名服务商修改域名服务器为cloudflare的,等cloudflare检测通过

# 域名服务器
braden.ns.cloudflare.com
shubhi.ns.cloudflare.com

接下来需要拿到我们的api key,点击cloudflare右上角 “我的个人资料” – “API令牌“

配置ca#ddy

在服务器 caddy/conf 文件夹下编写文件 caddy.service 文件内容为:

[Service]
Environment=CLOUDFLARE_EMAIL={你的邮箱地址}
Environment=CLOUDFLARE_API_KEY={刚拿到的cloudflare密钥}

接下来重启ca#ddy

docker restart caddy

此时用chrome打开网站,查看地址栏左侧是否提示锁的图标,如果不是也不用着急,有可能是域名解析还没有完全切换到新的域名服务器,这需要一定的时间。

Posted in 未分类 | Leave a comment

k8s服务访问

k8s基本网络模型

约法三单

  1. pod之间可以直接通信,无需显式使用NAT接收和地址转换
  2. node与pod之间可以直接通信,无需要明显地址转换
  3. pod可以看到自己的ip跟别人看到它所用的ip是一样的,中间不经过转换

分类

  1. underlay:与Host网络同层
  2. ovrelay:只要与Host网络不冲突,ip可以自由分配

Kubenets Service

metadata:
  name: my-service
spec:
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376

Service工作原理:

通过kube-proxy设置宿主机iptables规则来实现,kube-proxy通过观察service的创建,然后通过修改本机的iptables规则,将访问service vip的请求转发到真实的Pod上。

基于iptables规则的service的实现,宿主机上有大量Pod时,规则的不断刷新占用大量CPU资源,新的模式:ipvs,通过把规则放到内核态,降低了维护规则的代价

  1. Service的DNS记录:<myservice>.<mynamespace>.svc.cluster.local,访问这条记录时,返回的是Service的VIP或代理Pod的IP地址集合
  2. Pod的DNS记录:<pod_hostname>.<subdomain>.<mynamespace>.svc.cluster.local,注意pod的hostname和subdomain都是在Pod中定义的

集群内访问

`my-service:80 -> 192.168.11.236:9376` 

`my-service.my-namespace:80 -> 192.168.11.236:9376`

`172.18.255.16:80 -> 192.168.11.236:9376`

环境变量

TASK_BUS_SERVICE_HOST=172.18.254.152
TASK_BUS_SERVICE_PORT=80
TASK_WORKER_SERVICE_HOST=172.18.255.164
TASK_WORKER_SERVICE_PORT=80

Headless ServiceclusterIP: None不再提供虚拟IP来负载均衡

`my-service:9376 -> 192.168.11.236:9376,192.168.11.237:9376` 

`my-service.my-namespace:9376 -> 192.168.11.236:9376,192.168.11.237:9376`

集群外访问(外部宿主机访问)

  1. NodePort:外部client访问任意一台宿主机的8080端口,就是访问Servicer所代理的Pod的80端口,由接收外部请示请求的宿主机做转发,即client -> nodeIP:nodePort -> serviceVIP:port -> podIp:targetIp
  2. LoadBalance:公有云提供的k8s服务自带的loadbalancer做负载均衡和外部流量 的入口
  3. ExternalName:通过ExternalName或ExternalIp给Service挂在一个公有IP或者域名,当访问这个公有IP地址时,就会转发到Service所代理的Pod服务上。类似于软链或快捷方式
  4. ClusterIP:虚拟IP地址,外部网络无法访问,只有k8s内部访问使用。更像是一个伪造的IP网络
    • 仅仅用于Service这个对象,并由k8s管理和分配IP地址
    • 无法被Ping通,没有一个“实体网络对象”来响应
    • 只能结合Service Port组成一个具体的通信端口,单独的ClusterIP不具备通信的基础,并且它们属于k8s集群这样一个封闭的空间
    • 不同Service下的Pod节点在集群间相互访问可以通过ClusterIP

Service与Ingress

Ingress:全局的、为了代理不同后端Sercie而设置的负载均衡服务,只能工作在7层,而service工作在四层

Ingress对象:k8s项目对反向代理的一种抽象

Ingress Controller:目前支持Nginx、Haproxy、Envoy、Traefik等;nginx-ingress-controller只是被代理的Serive对象被更新,是不需要重新加载的,因为它的Lua方案实现了Nginx Upstream的动态配置。

nginx-igress-service:暴露服务出去,如果是公有云,需要创建LoadBalancer类型的Service

default-backend-service:默认规则

apiVersion: extensions/v1beat1
kind: Ingress
metadata:
  name: a-ingress
spec:
  tls:
  - hosts:
    # 标准域名格式,不能是IP地址
    - a.example.com
    secretName: a-secret
  rules:
  - hosts: a.example.com
    http:
    - path: /a1
      backend:
        serviceName: a1-svc
        servicePort: 80
    - path: /a2
      backend:
        serviceName: a2-svc
        sercvicePort: 80
Posted in Kubernetes | Tagged , | Leave a comment

k8s三层网络实现

字典

  1. 下一跳:如果ip包从主机A发送到主机B,需要经过路由设备X的中转,那么X的IP地址就应该配置为主机A的下一跳地址。
  2. Host-gw模式:将每个 Flannel 子网(Flannel Subnet,比如10.244.1.0/24)的下一跳 ,设置成该子网对应宿主机的IP地址。也就是主机(host)充当这条容器通信路径里的网关(gateway),也就是host-gw的含义;无封装,纯路由,只经过协议栈一次,性能较高
  3. 边际网关协议:BGP,Border Gateway Protocol,大规模数据中心维护不同“自治系统”之间路由信息、无中心的路由协议; 它不会在宿主上创建任何网桥设备相当于:每个边界网关上运行着一个小程序,它们会将各自的路由表信息,通过TCP传输给其它边界网关。其它边界网关上的这个小程序,则会对收到的这些数据进行分析,然后把需要的信息添加到自己的路由表。
    • CNI:与k8s对接的部分
    • Felix:DeamonSet,wmgm宿主机上插入路由规则,即写入Linux内核的FIB转发信息库,以及维护Calico所需要的网络设备等工作
    • BIRD:BGP客户端,专门负责在集群里分发路由信息
  4. 边际网关:负责把自治系统连接在一起的路由器。它的路由表中有其它自治系统里的主机路由信息;
  5. Calico:集群中所有的节点,都是边界路由器,被称为BGP Peer;默认情况下,是一个Node-to-Node Mesh的模式。随着节点的增多,会以N^2的规模快速增长。一般推荐在少于100个节点的集群中
  6. Calico Route Reflector的模式,大集群,它指定一个或几个专门节点,来负责与所有节点建立BGP连接,从而学习全局路由规则,其它节点,只需要给它交换路由信息。
  7. Calico IPIP模式:添加的路由规则10.233.2.0/24 via 192.168.2.2 tunl0,下一跳地址是Node2 IP地址,但发出去包的设备是tunl0(注意,不是flannel UDP模式的tun0,它们的功能是不同的)它是一个IP隧道设备(IP tunnel),IP包进入IP隧道后,被内核IPIP驱动接管。它会将这个IP包直接封装在一个宿主机网络的Ip包中。

Calico IPIP模式:

Flannel Host-gw模式

$ ip route
...
10.244.1.0/24 via 10.168.0.3 dev eth0

目的地址属于10.244.1.0/24网段的IP包,应该经过本机的eth0设备(dev eth0)设备发出去,并且它的下一跳(next-lop)是10.168.0.3(via 10.168.0.3)

从示意图上看,下一跳地址是目的宿主机Node2,所以IP包从应用层到链路层被封装成帧时,etho会使用下一跳地址对应的MAC地址,做为数据帧的MAC地址。

Node 2的内核网络从二层拿到IP包,就会看到这个目的IP地址:10.244.1.3,根据Node 2的路由表,会匹配到第二条路由规则,从而进入cni0网桥,进入infra-containeere-2当中。

Flannel子网和主机的信息,都是保存在Etcd中的。flanneld只需要wacth这些数据变化,然后实时更新路由表则可。性能损失在10%左右,其它基于VXLAN隧道机制的网络方案,性能损失在20%~30%左右。

Flannel host-gw限制,要求宿主机必须是二层连通;必须有其它宿主机的路由信息;

Posted in Kubernetes | Tagged , | Leave a comment

Pod生命周期

Pod容器

  1. Init Container:
    • 支持应用容器的全部字段与特性,包括资源限制,存储券和安全设置
    • 由于需要在pod就绪之前运行完成,所以不支持reaadinessProbe
    • 定义多个init容器,它们会按定义顺序执行
    • init容器失败,如果restartPolicy不为Nerver,则会不断重启,直到成功为止
    作用:
    • 具有与应用容器分离的单独镜像,可包含不建议在生产镜像中包含的实用工具
    • 应用程序镜像基于它可以分离出创建和部署的角色
    • 使用linux namespace,相对应用容器来说有不同的文件系统视图,因此有访问secret的权限,应用容器则不能
    • 可以阻塞或延迟应用容器的启动
  2. Pod Hook 如果postStart,postStop失败,会杀死容器
    • postStart:容器创建后立即执行,不保证钩子在容器EntryPoint之前运行。主要用于资源部署、环境准备等。如果运行时间过长以至不能运行或者挂起容器将无法到达Running状态
    • preStop:容器终止之前立即调用,阻塞,同步的,必须在删除容器调用发出之前完成。主要用于优雅关闭应用程序、通知其它系统等。如果执行期间被挂起,pod将永远在running状态,并不会到达failed状态
  3. 健康检查

Pod状态

PodStatus.phase字段

  1. 挂起(Pending)信息已提交集群,但没有被调度器调度到合适的节点或pod镜像正在下载
  2. 运行中(Running)已绑定到一个节点,所有容器已被创建。至少一个正在运行,或者处理启动或重启状态
  3. 成功(Successed)所有容器成功终止,并且不会重启
  4. 失败(Failed)所有容器已终止,并且至少一个容器是因为失败终止,也就是说容器以非0状态退出或被系统终止
  5. 未知(UnKnow)无法获得状态,通常是主机通信失败导致的

PodStatus.PodCondition

描述当前Status的具体原因

  1. PodScheduled:pod已调度到某节点
  2. ContainersReady:pod内所有容器已就绪
  3. Initialized:所有的init容器都已成功完成
  4. Unschedulable
  5. Ready:可以提供服务,并且应该被添加到对应服务的负载均衡池中

Pod

  1. 资源定义(CPU、内存)
  2. 调度
  3. Volume
    • Projected Volume 投射数据卷 存在不是为了存放容器中的数据,也不是容器与宿主之间数据交换,为容器提供预先定义好的数据
      • Secret
      • ConfigMap
        • 不需要加密的,应用所需要的配置文件
      • Downward API
        • 获取到Pod API对象本身的信息,进程启动之前就能确定的信息
      • SeviceAccountToken

restartPolicy

  1. Always:容器失效时,kubelet自动重启
  2. OnFailure:终止运行并且退出码不为0,由kubelet自动启动
  3. Never:不论容器运行状态如何,都不重启

ReplicationController,DaemonSet必须为always,需要保证容器持续运行

Job:onFailure或Never,确保执行完成后不再重启

探针

readinessProbe

检查结果决定这个Pod能不能通过Service的方式访问到,而不会影响Pod的生命周期。探测失败,端点控制器将从与 pod匹配的所有服务的端点列表中删除这个pod的ip地址

livenessProbe

配置参数太多,有模板:PodPresent。指示容器是否存活,如果探活失败,则kubelet会杀死容器,并且通过重启策略决定未来

startupProbe 指示容器内应用是否已经启动,提供了此探针,则所有其它控针都会被禁用,直到这个探针成功为止。

容器状态 kubectl describe pod <pod名字>

  1. Waiting:等待,仍在运行完成它启动所需要的操作,比如拉镜像、应用secret数据等
  2. Running:运行中,容器正在执行,并且没有问题产生
  3. Terminated:已终止,容器已经执行并且或者正常结束,或者因为其它原因失败

pod终止

体面退出

容器运行时会向每个容器的主进程发送TERM信号,一旦超出了体面退出终止限期,容器运行时会向所有剩余进程发送kill信号,之后pod会从api服务器上移除

例子:

  1. 使用kubectl手动删除某个pod,pod的体面终止时间默认为30s
  2. api服务中pod对象被更新,pod状态会被标记为“terminating“正在终止,超出所计算的体面终止限期时间点则认为pod dead
  3. kubelet看到pod被标记为正在终止(已设置了体面终止限期),则马上开始本地pod关闭过程
    • 检查并执行preStop回调,如果到达体面终止时间,仍在运行,则一次性给予宽限期2秒(可通过terminationGracePeriodSeconds参数配置)
    • 接下来kubelet触发容器运行时发送TERM信号给容器中的进程,如果需要容器按顺序关闭,可以考虑使用preStop回调逻辑来协调
  4. 控制器会将pod从对应的端点列表中移除,ReplicaSets与其它工作负载资源不再将关闭进程中的pod视为合法,可提供服务的副本。服务在终止宽限期开始的时候负载均衡器已经从端点列表肿移除了,所以pod即使还没有关闭完成,也无法继续处理请求
  5. 超出宽限期,kubelet会触发强制关闭,容器进行时向所有运行中容器发送sigkill信息。同时如果使用了pause容器的话,kubelet也会清理隐藏的pause容器
  6. api服务器删除pod的api对象,所有客户端之后则无法再看到这个容器

强制关闭

宽限期限--grace-period=<seconds>设置为0,并且设置--force参数,api服务器直接删除pod对象,节点侧,被终止的pod仍然会在被强制终止前获得一点点宽限时间

Posted in Kubernetes | Tagged , | Leave a comment

qcow2镜像定制指南

背景

目前网络上关于定制镜像的说明很分散,需要搜寻很多文章才能完成镜像的定制任务。所以我尝试提供一个全面而系统的指南,遵循本指南,用户可以方便的完成镜像的定制。

实施步骤

一、环境配置

1、准备软件
mac pro、VmWare fusion、CentOS-7-x86_64-DVD-1708.iso、CentOS-7-x86_64-GenericCloud-1708-20180123.qcow2
2、安装嵌套CentOs环境
由于MacOs不支持Kvm,故需要在嵌套的操作系统中安装云镜像需要的软件,使用Fusion很容易在MacOs中虚拟出一个CentOs的环境。
3、修改嵌套操作系统配置
在centos关闭的情况下打开虚拟机“处理器和内存”配置,选择“高级配置”,选中“在此虚拟机中启用虚拟化管理程序”和“在此虚拟机中启用代码分析应用程序”,如无这步操作,则在启动virt-manager时会报:“
virt-manager 报 WARNING : KVM不可用.这可能是因为没有安装KVM软件包,或者没有载入KVM内核模块.您的虚拟机可能性很差。”的错误,启动虚拟机。以下操作如无特殊说明都是在嵌套操作系统中执行。
4、安装依赖

yum install qemu-kvm qemu-img qemu-kvm-tools qemu-kvm-common
yum install libvirt-admin libvirt-client libvirt-daemon libvirt-devel libvirt
yum install libguestfs libguestfs-tools libguestfs-bash-completion

5、编译nbd内核模块(如不使用“nbd挂载方式修改镜像”则不需要安装此模块)
执行命令,出现以下报错时,说明没有nbd模块,需要自己手动安装





执行下面的命令安装hbd模块

[@xx] # cat /etc/redhat-release
CentOS Linux release 7.4.1708 (Core)
[@xx] # uname -a
Linux localhost 3.10.0-693.el7.x86_64 #1 SMP Tue Aug 22 21:09:27 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
[] # uname -r
3.10.0-693.el7.x86_64

安装kernel组件

[@xx] sudo yum install kernel-devel kernel-headers

编译安装hbd组件

# 下载对应的内核源码包
[@xx] # wget http://vault.centos.org/7.4.1708/os/Source/SPackages/kernel-3.10.0-693.el7.src.rpm
[@xx] # rpm -ihv kernel-3.10.0-693.el7.src.rpm
[@xx] # cd /root/rpmbuild/SOURCES/
[@xx] # tar Jxvf linux-3.10.0-123.el7.tar.xz -C /usr/src/kernels/
[@xx] # cd /usr/src/kernels/
# 配置源码
[@xx] # mv $(uname -r) $(uname -r)-old
[@xx] # mv linux-3.10.0-693.el7 $(uname -r)
# 编译安装
[@xx 3.10.0-693.el7.x86_64] # cd $(uname -r)
[@xx 3.10.0-693.el7.x86_64] # make mrproper
[@xx 3.10.0-693.el7.x86_64] # cp ../$(uname -r)-old/Module.symvers ./
[@xx 3.10.0-693.el7.x86_64] # cp /boot/config-$(uname -r) ./.config
[@xx 3.10.0-693.el7.x86_64] # make oldconfig
[@xx 3.10.0-693.el7.x86_64] # make prepare
[@xx 3.10.0-693.el7.x86_64] # make scripts
[@xx 3.10.0-693.el7.x86_64] # make CONFIG_BLK_DEV_NBD=m M=drivers/block
[@xx 3.10.0-693.el7.x86_64] # cp drivers/block/nbd.ko /lib/modules/$(uname -r)/kernel/drivers/block/
[@xx 3.10.0-693.el7.x86_64] # depmod -a
# 查看nbd模块
[@xx 3.10.0-693.el7.x86_64]$ modinfo nbd
filename:       /lib/modules/3.10.0-693.el7.x86_64/kernel/drivers/block/nbd.ko
license:        GPL
description:    Network Block Device
rhelversion:    7.4
srcversion:     EDE909A294AC5FE08E81957
depends:        
vermagic:       3.10.0 SMP mod_unload modversions 
parm:           nbds_max:number of network block devices to initialize (default: 16) (int)
parm:           max_part:number of partitions per device (default: 0) (int)
parm:           debugflags:flags for controlling debug output (int)

编译安装时的错误处理
阶段:make CONFIG_BLK_DEV_NBD=m M=drivers/block

drivers/block/nbd.c: 在函数‘__nbd_ioctl’中:
drivers/block/nbd.c:619:19: 错误:‘REQ_TYPE_SPECIAL’未声明(在此函数内第一次使用)
   sreq.cmd_type = REQ_TYPE_SPECIAL;
                   ^
drivers/block/nbd.c:619:19: 附注:每个未声明的标识符在其出现的函数内只报告一次
make[1]: *** [drivers/block/nbd.o] 错误 1
make: *** [_module_drivers/block] 错误 2

处理:

[@xx 3.10.0-693.el7.x86_64] # vim include/linux/blkdev.h
# 由代码可知 REQ_TYPE_SPECIAL = 7
/*
 * request command types
 */
enum rq_cmd_type_bits {
        REQ_TYPE_FS             = 1,    /* fs request */
        REQ_TYPE_BLOCK_PC,              /* scsi command */
        REQ_TYPE_SENSE,                 /* sense request */
        REQ_TYPE_PM_SUSPEND,            /* suspend request */
        REQ_TYPE_PM_RESUME,             /* resume request */
        REQ_TYPE_PM_SHUTDOWN,           /* shutdown request */
#ifdef __GENKSYMS__
        REQ_TYPE_SPECIAL,               /* driver defined type */
#else
        REQ_TYPE_DRV_PRIV,              /* driver defined type */
#endif
        /*
         * for ATA/ATAPI devices. this really doesn"&gt;cmd[0] with the range of driver
         * private REQ_LB opcodes to differentiate what type of request this is
         */
        REQ_TYPE_ATA_TASKFILE,
        REQ_TYPE_ATA_PC,
};
# 修改nbd.c文件
[@xx 3.10.0-693.el7.x86_64] # vim drivers/block/nbd.c
# sreq.cmd_type = REQ_TYPE_SPECIAL;
sreq.cmd_type = 7;
# 重新执行命令
[@xx 3.10.0-693.el7.x86_64] # make CONFIG_BLK_DEV_NBD=m M=drivers/block

二、设置镜像共享

设置嵌套虚拟机文件夹共享
qcow2文件放置在mac本地文件夹中,嵌套虚拟机通过文件共享的方式使用qcow2文件。需要注意的是qcow2文件权限需要在macos中设置为可读写,否则在嵌套虚拟机中无法更新配置。

[[email protected]] # chmod 755 ./CentOS-7-x86_64-GenericCloud-1708-20180123.qcow2

嵌套虚拟机中,需要要关闭SeLinux否则同样无法更新镜像内容

sudo /usr/sbin/setenforce 0

三、guestfish工具使用

1、示例程序:获取镜像ip地址

guestfish --rw -a ./CentOS-7-x86_64-GenericCloud-1708-20180123.qcow2
# run
# list-filesystems
/dev/sda1: xfs
# mount /dev/sda1 /
# vi /var/log/cloud-init.log

2、示例程序:配置用户访问权限

guestfish --rw -a ./CentOS-7-x86_64-GenericCloud-1708-20180123.qcow2
# run
# list-filesystems
/dev/sda1: xfs
# mount /dev/sda1 /
# mkdir /home/dc2-user/.ssh
# 注意guestfish与shell的区别,权限位是四位
# chown 1001 1001 /home/dc2-user/.ssh
# chmod 0700 /home/dc2-user/.ssh
# touch /home/dc2-user/.ssh/authorized_keys
# chmod 0600 /home/dc2-user/.ssh/authorized_keys
# chown 1001 1001 /home/dc2-user/.ssh/authorized_keys
# ll /home/dc2-user/.ssh/authorized_keys
-rw------- 1 1001 1001 0 Mar  1 10:05 /sysroot/home/dc2-user/.ssh/authorized_keys
# vi /home/dc2-user/.ssh/authorized_keys
# 添加公钥...
# quit

四、nbd挂载方式修改镜像(qemu-nbd)

1、确保已安装nbd模块,加载模块

# modinfo nbd
# insmod /lib/modules/3.10.0-693.el7.x86_64/kernel/drivers/block/nbd.ko max_part=8

3、建立nbd连接,挂载到目录

# qemu-nbd -c /dev/nbd0 ./CentOS-7-x86_64-GenericCloud-1708-20180123.qcow2
# mkdir -p /mnt/qcows/qcow0
# mount /dev/nbd0p1  /mnt/qcows/qcow0

4、执行chroot

# chroot /mnt/qcows/qcow0/

5、执行修改,比如

$ passwd dc2-user
# 或其它操作

6、修改完毕后解除挂载点,解除连接

# umount /mnt/qcows/qcow0
# qemu-nbd -d /dev/nbd0p1

五、通过virt-manager挂载虚拟机

1、执行

virt-manager

2、新建虚拟机
选择“导入现有磁盘”,“使用ISO镜像”,选择qcow2文件…
如果报:“WARNING : KVM不可用.这可能是因为没有安装KVM软件包,或者没有载入KVM内核模块.您的虚拟机可能性很差。”的警告相应的解决方案是:

  • 关闭虚拟机
  • 进入虚拟机设置(可以配置网卡,硬盘,光驱的地方)
  • 点击“处理器和内存”,勾选虚拟化Inter VT-x/EPT 或AMD-V/RVI(V)

3、登录机器,修改
IP地址和密码、公钥方式登录,在前面已说明如何操作

六、清理痕迹

1、清理/var/log/文件夹
2、删除cloud-init执行记录
cloud-init是专门为云环境的虚拟机初始化而开发的工具,通过读取相应的数据,对虚拟机进行配置。其执行完毕后会在一个叫 sem 的目录下创建信号文件,以便下次启动模块时不再重复执行语句。其目录位置为:
/var/lib/cloud/instances/实例ID/sem

sudo rm -rf /var/lib/cloud/instances
sudo rm -rf /var/lib/cloud/instance

3、清理history和.ssh目录等

rm -rf /home/xxx/.ssh
echo '' # /home/xxx/.bash_history
echo '' # /root/.bash_history
 
rm -rf /root/.oracle_jre_usage

七、去除磁盘空洞

# 创建同样大小的镜像
$ qemu-img create -f qcow2 CentOS-7-x86_64-GenericCloud-1708-20180329.qcow2 40G
$ virt-sparsify -x ./CentOS-7-x86_64-GenericCloud-1708-20180123.qcow2 --convert qcow2 ./CentOS-7-x86_64-GenericCloud-1708-20180329.qcow2
$ du -sh *
7.3G	CentOS-7-x86_64-GenericCloud-1708-20180123.qcow2
5.3G	CentOS-7-x86_64-GenericCloud-1708-20180329.qcow2

Posted in 未分类 | Leave a comment

Sqoop源码分析

Sqoop的Mysql数据导出实现分两种,一种是使用JDBC方式从Mysql中获取数据,一种是使用MysqlDump命令从MySql中获取数据,默认是 JDBC方式获取数据,如果要使用dump方式获取数据,需要添加 -direct 参数。

使用JDBC方式从Mysql中获取数据

配置语句时,需要添加 $CONDITIONS 点位符,比如:SELECT id FROM user WHERE $CONDITIONS,Sqoop在内部实现时会把它替换成需要的查询条件。

Sqoop启动后会先查询元数据,它会把 $CONDITIONS 替换为 (1=0) ,然后用得到的SQL语句查询数据表对应的Meta信息对于导出一个表的情况,Sqoop会使用这个SQL查询三次数据库,分别是: 1、获取 colInfo(最终得到columnTypes信息)2、查询ColumnNames信息3、生成QueryResult类执行 generateFields操作获取columnTypeNames时。

Sqoop会对获取的Fields做校验,列不能重复,它还会处理数据库的字段到Java属性名的转换

QueryResult类是通过构建java类文件,然后获取JavaCompiler,然后编译加载,为了提高处理性能,不是使用反射实现的,这个生成类内部处理mysql到hdfs属性值为空和分隔符的处理。

接着它会进行下面一个Sql查询操作,查询结果集为MIN(split列),MAX(split列),查询条件的处理逻辑为 $CONDITIONS 替换为(1=1),然后再添加外面SELECT查询 (举例:SELECT MIN(id), MAX(id) FROM (SELECT ID,NAME,PASSPORT WHERE (1=1) ) AS t1 ),这样就查询出来此次导出数据最大的split列值和最小的split列值。

对于为整数、布尔值、时间格式、Float等 的分区列,进行split时直接根据对应值的大小进行Split,Text文本的处理方式比较特殊,Sqoop先会对之前获取到的Min和Max的字串寻找它们最大的相同前缀子字串,然后把后面的字段转化为BigDecimal,结合时char占两个字节(65536),算法在 TextSplitter类中,比较简单,本质上就是一个进制转换。拆分好后,需要把Split的值再转换为String,然后加上相同前缀子字段,就构成了查询区间了(注意中文可能会被拆分)。

Sqoop对数据的获取是在DataDrivenDBRecordReader中,在查询时会把 $CONDITIONS 替换成 split 的范围比如 ( id >= 1) && (id<10),使用JDBC获取到结果集游标,然后移动游标处理数据。

使用MysqlDump命令从MySql中获取数据

第二种方法与第一种方式有下面的差别:

初始化元数据,它是在构建的查询语句后面添加 limit 1 ,比如:SELECT t. FROM AS t LIMIT 1,因为dump方式在查询指定获取列时使用的是 t.,当使用limit 0时,数据库不会给它返回必须的元数据信息。

dump方式在map进行数据的获取,其会构建mysqldump命令,然后使用java程序调用,获取输入输出流和错误流,其实现了 org.apache.sqoop.util.AsyncSink 抽象类,用来处理输入输出流和错误流。

优化策略:

Sqoop在执行时会执行三次相同的Sql语句用来查询无数据,可以合并查询,由于查询仅返回Meta信息很快,不需要修改这块的实现。

分区列选择对于查询元数据和导出的查询影响很大,应该对索引做调优,避免对分区列的排序操作,加快元数据查询速度和导出数据的速度,尽量选择自增加的主键ID做Split列,区分度好并且可以顺序读取数据。

导出操作的查询语句中,$CONDITIONS 会被替换为范围区间,创建索引时,要考虑做这个查询的优化。

索引建议,考虑三个规则(使查询数据集较少、减少点的查询、避免排序操作),Sqoop场景下,如果分区列不是主键(自增加)时,把分区列做为联合索引的第一个字段,其它被选择的查询条件做为索引的其它字段。

分区列的选择,要避免Split后数据不均衡。

从实现上来看-m参数是可以增加任务的并行度的,但数据库的读线程是一定的,所以 -m 过大对于数据库和hadoop集群会有压力,在Sqoop的场景下,数据库是一个影响并发的瓶颈。增加mapper数意义不大。

下面列出Sqoop目前1.4.6版本存在的两个问题。

查看Sqoop源码,发现其存在两个比较严重的问题。

问题 1、数据分片与Mapper设定不合理

Sqoop在抽取时可以指定 -m 的参数来控制mapper的数量的,sqoop根据mapper的数量,对数据库数据进行分片,每个分片交给一个mapper处理,mapper在处理的过程中会通过jdbc或mysqldump对相应分片的数据进行查询和拉取,最终产生和mapper数量相同的文件数。

当每天需求导入hdfs中的表非常大时,过小的mapper值,会导致写入hdfs中的文件过大,如果文件的格式不能被split,那么会对业务下游产生影响。过大的mapper值,在数据库读线程一定,网络带宽一定的情况下,并不能提高导入hdfs的效率,反而会对数据库造成并发读压力,同时过多的mapper也会占用hadoop集群的资源。

Sqoop做为一个数据导出框架,对数据的控制应该再细至一些,-m 只是控制mapper的数量,而数据分片数目应该由另外的参数控制。

我们为sqoop添加了 -split-per-map 参数,比如设置-m=4 -split-per-map=2,则对结果集分 8 片,每个Mapper处理两片数据,最后共产生 8 个文件。

问题 2、分片效率低

Sqoop在做分片处理时有问题,其实现会使用 “Select Max(splitKey),Min(splitKey) From ( –select参数 ) as t1” 的语句来查询分片信息,在Mysql下,这样的查询会产生一个以split-id为主键,包含需要导出的其它所有字段的临时表,如果数据量不大,临时表数据可以在内存中,处理速度还可以保证。但如果数据量很大,内存中已经存放不下时,这些数据会被保存为MyISAM表存放到磁盘文件中,如果数据量再大一些,磁盘文件已经存放不下临时表时,拆分数据会失败。

在我们场景下,由于表数据量很大,这个查询占到整个导出时间的45%(整个过程原来需要90多分钟),优化空间很大。

解决方案一:

修改所有导出脚本,分片语句自定义

解决方案二:

修改:org.apache.sqoop.mapreduce.DataDrivenImportJob的

@Contract(“null, _ -&gt; !null”)
private String buildBoundaryQuery(String col, String query)

修改代码如下

org.apache.sqoop.mapreduce.DataDrivenImportJob的
 
@Contract("null, _ -> !null") 
private String buildBoundaryQuery(String col, String query)
 
/**
   * Build the boundary query for the column of the result set created by
   * the given query.
   * @param col column name whose boundaries we're interested in.
   * @param query sub-query used to create the result set.
   * @return input boundary query as a string
   */
  private String buildBoundaryQuery(String col, String query) {
    if (col == null || options.getNumMappers() == 1) {
      return "";
    }
 
    // Replace table name with alias 't1' if column name is a fully
    // qualified name.  This is needed because "tableName"."columnName"
    // in the input boundary query causes a SQL syntax error in most dbs
    // including Oracle and MySQL.
    String alias = "t1";
    int dot = col.lastIndexOf('.');
    String qualifiedName = (dot == -1) ? col : alias + col.substring(dot);
 
    ConnManager mgr = getContext().getConnManager();
    String ret = mgr.getInputBoundsQuery(qualifiedName, query);
    if (ret != null) {
      return ret;
    }
 
//    return "SELECT MIN(" + qualifiedName + "), MAX(" + qualifiedName + ") "
//        + "FROM (" + query + ") AS " + alias;
    return initBoundaryQuery(qualifiedName, query, alias);
  }
 
  private String initBoundaryQuery(String qualifiedName, String query, String alias) {
    StringBuilder regex = new StringBuilder();
    regex.append("(\\s[A|a][S|s][\\s][`]?");
    for (char c : qualifiedName.toCharArray()) {
      regex.append('[').append(c).append(']');
    }
    regex.append("[`|\\s|,])");
    final Matcher matcher1 = Pattern.compile(regex.toString()).matcher(query);
    final boolean asCheckOk = !matcher1.find();
    if(asCheckOk) {
      final Matcher matcher2 = Pattern.compile("(\\s[F|f][R|r][O|o][M|m]\\s)").matcher(query);
      int count = 0;
      while (matcher2.find()) {
        count++;
      }
      boolean fromCheckOk = count == 1;
      if(fromCheckOk) {
        final Matcher matcher = Pattern.compile("(\\s[F|f][R|r][O|o][M|m]\\s[\\s\\S]*)").matcher(query);
        while (matcher.find()) {
          return "SELECT MIN(" + qualifiedName + "), MAX(" + qualifiedName + ") "
                  + matcher.group();
        }
      }
    }
    return "SELECT MIN(" + qualifiedName + "), MAX(" + qualifiedName + ") "
            + "FROM (" + query + ") AS " + alias;
  }

问题三:无法自定义特殊字符替换

解决方案一:

通过SQL的替换功能,修改脚本代价高,并且脚本可读性、可维护性都大大降低

解决文案二:

修改Sqoop实现,增加自定义特殊字符替换功能

Posted in 未分类 | Leave a comment