百种弊病,皆从懒生

snet dev note: support MacOS

这两天得了空, 让 snet 支持了下 MacOS.

snet 的大致原理是通过系统防火墙的流量重定向功能,将所有去往国外的流量导到 snet 监听的端口, 在程序内部 将流量传递给上游的 proxy server(ss, http), 拿到响应后再回给客户端.

实现关键是要在 snet 内部获取到流量的原目标地址, 因为重定向之后 tcp connnection 的目标地址变成了 snet 监听 的地址.

Linux 上的实现,以前讲过: https://blog.monsterxx03.com/2019/03/31/snet-transparent-ss-proxy-on-linux/ 是通过 SO_ORIGINAL_DST 这个 socket option 实现的.

MacOS 上没有 iptables, 类似的工具是系统自带的 pfctl, 捣鼓了一下也能实现一样的功能.

用 pfctl 做流量重定向

pfctl 的文档可以通过 man pfctl, man pf.conf 查看. 我也只是看了个大概, 细节并不清楚.

......

Random Talk

Just some random complains and notes about server infra management. I think those are my motivations to move to kubernetes.

Won’t explain k8s or docker in detail, and how they solve those problems in this post.

Infrastructure level(on AWS)

We use following services provided by AWS.

  • Compute:
    • EC2
    • AutoScaling Group
    • Lambda
  • network:
    • VPC (SDN network)
    • DNS (route53)
    • CDN (CloudFront)
  • Loadbalancer:
    • ELB (L4)
    • NLB (L4, ELB successor, support static IP)
    • ALB (L7)
  • Storage:
    • EBS (block storage)
    • EFS (hosted NFS)
    • RDS(MySQl/PostgreSQL …)
    • Redshift (data warehouse)
    • DynamoDB (KV)
    • S3 (object storage)
    • Glacier (cheap archive storage)
  • Web Firewall (WAF)
  • Monitor (CloudWatch)
  • DMS (ETL) …

For infra management, in early days, we just click, click, click… or write some simple scripts to call AWS api.

With infra resources growing, management became complex, a concept called Infrastructure as Code rising.

AWS provides CloudFormation as orchestration tool, but we use terraform (for short: CloudFormation sucks, for long: Infrastructure as Code)

So far, not bad.(tweak those services internally is another story… never belive work out of box)

Application level

  • configuration management (setup nginx, jenkins, redis, twemproxy, ElasticSearch or WTF..)
  • CI/CD
  • dependency management

They’re complicated, people developped bunch of tools to handle: puppet, chef, ansible, saltstack …

They’re great and working, but writing correct code still a challenge when changes involves:

......

Centralized Logging on K8S

搞定了监控, 下一步在 k8s 上要做的是中心化日志, 大体看了下, 感兴趣的有两个选择: ELK 套件, 或fluent-bit + fluentd.

ELK 那套好处是, 可以把监控和日志一体化, filebeat 收集日志, metricbeat 收集 metrics, 统一存储在 ElasticSearch 里, 通过第三方项目elastalert 可以做报警,也能在 kibana 里集成界面. 坏处是 ElasticSearch 存储成本高, 吃资源. 我们对存储的日志使用需求基本就是 debug, 没有特别复杂的BI需求, 上一整套 ELK 还是太重了.

选择 fluent-bit + fluentd 还有个好处是, 之前内部有套收集 metrics(用于统计 DAU, retention 之类指标) 的系统本来就是基于 fluentd 的, 用这套就不用改 metrics 那边的 ETL 了.

......

Prometheus on K8S

Why move to prometheus?

把生产环境迁移到 k8s 的第一步是要搞定监控, 目前线上监控用的是商业的 datadog, 在 container 环境下 datadog 监控还要按 container 数目收费, 单 host 只有 10 个的额度, 超过要加钱, 高密度部署下很不划算. 一个 server 跑 20 个以上 container 是很正常的事情, 单台 server 的监控费用立马翻倍.

tracing 这块之前用的也是 datadog, 但太贵了,一直也想换开源实现, 索性监控报警也换了, 踩一把坑吧.

vendor lock 总是不爽的…

Metrics in k8s

先不提 prometheus, k8s 中 metrics 来源有那么几个:

metrics-serever

metrics-server (取代 heapster), 从 node 上 kubelet 的 summary api 抓取数据(node/pod 的 cpu/memory 信息), kubectl top 和 kube-dashboard 的 metrics 数据来源就是它, horizontal pod autoscaler 做 scale up/down 决策的数据来源也是它, metrics-server 只在内存里保留 node 和 pod 的最新值.

......

kubeconfig 和 aws named profile 管理的 tips

我有两个 EKS 集群 (sandbox + production), 这两个集群分处两个 aws 帐号中. 所以管理的时候也需要两套 aws credential.

同时我用 helm-secrets 来管理 helm charts 中需要加密的一些配置. helm-secrets 只是 sops 的一个 shell wrapper, 实际加密是通过 sops 进行的.

sops 支持 aws KMS, gcp KMS, azure key vault.. 等加密服务. 我用的是 aws KMS, 在 KMS 里创建一个 key, 授权允许我这个 iam 帐号能用它来进行加解密.

这带来了一个问题, kubectl 和 helm-secrets 都需要 aws credential, 如果两边用的不一样就会执行失败.

我统一使用 aws 的 named profiles 来管理 credential. 不在环境变量里设 aws 的 access key/secret key(如果设置了, 优先级比 named profile 高)

目录结构:

......

Jenkins on K8S

最近在把 jenkins 迁移到 k8s, 具体怎么 setup 的不赘述了(helm chart, jenkins home 目录挂pvc, jenkins kubernetes-plugin).

jenkins 跑 k8s 好处是可以方便得做分布式 build, 每次 trigger 一个 job 的时候自动起一个 pod 作为 jenkins slave agent, 结束了自动删掉. 在 aws 上结合 cluster-autoscaler 可以极大得扩展 ci 的并行能力, 降低成本.

记录一点过程中的坑.

装上 kubernetes-plugin 后,要想让 jenkins 的 job 在 pod 中跑, 必须用 pipeline 的方式编写 job 定义. script 和 declarative 两种方式都支持启动 pod. 如果用 shell 的方式编写, 不会跑在 pod 里,会直接在 master 的 workspace 里 build.

declarative 方式的例子:

pipeline {
    agent {
        kubernetes  {
            label 'test-deploy'
            yamlFile 'test-deploy.yaml'
        }
    }
    stages {
        stage('stage test') {
            steps('tests') {
                container('test') {
                    sh 'ls'
                }
            }
        }
    }
}

ls 命令就会在 test container 里执行, 如果不用 container() step 的话,默认会在 pod 的 default container 里执行.

......

K8s Volume Resize on EKS

从 k8s 1.8 开始支持 PersistentVolumeClaimResize. 但 api 是 alpha 状态, 默认不开启, eks launch 的时候版本是 1.10, 因为没法改 control plane, 所以没法直接在 k8s 内做 ebs 扩容. 后来升级到了 1.11, 这个 feature 默认被打开了, 尝试了下直接在 EKS 内做 ebs 的扩容.

注意:

  • 这个 feature 只能对通过 pvc 管理的 volume 做扩容, 如果直接挂的是 pv, 只能自己按传统的 ebs 扩容流程在 eks 之外做.
  • 用来创建 pvc 的 storageclass 上必须设置 allowVolumeExpansion 为 true

在 eks 上使用 pv/pvc, 对于需要 retain 的 volume, 我一般的流程是:

  • 在 eks 之外手工创建 ebs volume.
  • 在 eks 中创建 pv, 指向 ebs 的 volume id
  • 在 eks 中创建 pvc, 指向 pv

示例 yaml:

---
kind: PersistentVolume
apiVersion: v1
metadata:
  name: test
spec:
  storageClassName: gp2
  persistentVolumeReclaimPolicy: Retain
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 5Gi
  awsElasticBlockStore:
    fsType: ext4
    volumeID: vol-xxxx   # create in aws manually
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  volumeName: test

假如现在要扩容到 10Gi, 流程是:

......

snet dev note

完成 SNET 初版后又做了些后续更新, 记录一点.

支持 http tunnel

配置文件里增加一个 proxy-type 选项, 默认为 ss, 可改成 http, 这样可以将 支持 http tunnel 的代理服务器作为 upstream(例如 squid). 填上 http-proxy- 开头 的选项就行.

实现上 client 端要对接 http tunnel 非常简单:

  • client 发送请求: Connect tgt-host:tgt-port HTTP/1.1
  • server response: HTTP/1.1 200, 即表示 server 端支持 http tunnel
  • client 后续向该 tcp connection 写入的数据都会被 server 转发到 tgt-host:tgt-port

改动的时候把 upstream proxy 的部分重构了一下, 抽了个 Proxy interface 出来, 后续想对接其他协议方便扩展.

对 udp 支持的尝试

对 tcp 流量的转发能通过 iptables REDIRECT 实现的, 通过 getsockoption 可以知道 tcp connection 的原目标, 但这对 udp 行不通, REDIRECT 之后拿不到原 target.

......

snet: transparent ss proxy on Linux

日常使用 Linux 工作, Linux 下实现全局透明代理可以用 iptables + ss-redir, 要有比较好的上网体验还需要 ChinaDNS 配合 dnsmasq, 这一整套在路由器上搞一遍就算了, 在本地太麻烦了. 仔细想想这几个加起来的功能实现起来也并不复杂, 前阵子就写了个小东西, 用一个进程完成全局透明代理 + ChinaDNS + 国内外分流: https://github.com/monsterxx03/snet

目前的限制:

  • 不支持 ipv6
  • 只支持 tcp (因为我的测试服务器不支持 udp, 以后再加上吧)
  • 上游 server 只支持 ss

目的是一个进程 + 一个配置文件完成所有事情. 需要的 iptable 规则也全部内置了(包括 CN ip 段), 缺少灵活但对我够用了, 以后有需要再加上选项不自动配吧.

......

Celery Time Limit 的坑

之前用 celery 做的 task 都是一些很简单轻量级的 task, 从来没触发过 timeout, 最近加入了一些复杂很耗时的 task, 碰到一些 time limit 的坑.

celery 中 time limit 有两种, soft_time_limit 和 hard_time_limit, 区别是 soft_time_limit 会在内部抛一个 Exception, task 可以 catch 自行处理. hard time limit 没法被 catch.

使用如下:

from myapp import app
from celery.exceptions import SoftTimeLimitExceeded

@app.task
def mytask():
    try:
        do_work()
    except SoftTimeLimitExceeded:
        clean_up_in_a_hurry()

我 celery pool 用的是 gevent, 实际上在现在的实现里 gevent 做 worker pool 的时候会忽略 soft_time_limit, 只有 hard_time_limit 会被触发(通过 gevent.Timeout 实现).

坑爹的是文档里写的是错的: http://docs.celeryproject.org/en/latest/userguide/workers.html#time-limit

soft_time_limit 只在 prefork pool 里支持.

我现在想让 celery 把这个 hard timeout 的情况 report 到 sentry, 看了圈代码并没法从外面 override timeout 的 callback. 只能很丑得做了个 monkey patch, 在初始化 celeryapp 的代码里:

......