Golang 编译最小化的 Docker Image

普通做法

以下是引用自某开源项目的做法:

FROM golang:alpine  
RUN apk update && \  
    apk upgrade && \
    apk add git
RUN go get github.com/author/package  

这样产生的包大约是 100MB,已经很不错了,但是还是浪费了大量空间。

比如 Golang SDK 在程序运行中根本用不到,Go Get带来的源文件同样用不到。Golang是可以编译到真二进制的的语言,只要保留程序本身和必要的动态链接库(.so)即可。进一步,如果使用静态链接,那库也可以不要。

From Alpine

首先编译出静态链接的二进制:

CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o build/release/main ./main.go  

然后是Dockerfile,把刚刚编译出的程序拷贝进去就可以了。其中用到 APK (Alpine Package Manager) 安装CA证书,因为Golang标准库中的SSL会使用系统的CA证书。

FROM alpine:3.4  
RUN apk add --no-cache ca-certificates

ADD ./build/release/main /  
ENTRYPOINT ["/main"]  

From Scratch

其实,Alpine Linux也可以不要,直接从0开始Build一个Docker Image也是可行的,理论上这是小的方案。

同上,先编译出静态链接的二进制。Dockerfile如下:

FROM scratch  
ADD ca-certificates.crt /etc/ssl/certs/

ADD ./build/release/main /  
ENTRYPOINT ["/main"]  

ca-certificates.crt 通常位于系统的/etc/ssl/certs/目录下,你需要从那里把他拷贝到当前目录,然后添加到镜像中。


为了方便使用,我把以上两种方案都build好放在了Docker Hub上

直接在Dockerfile里FROM fuyufjh/go-alpine或者FROM fuyufjh/go-scratch即可。

References

  1. Building Minimal Docker Containers for Go Applications
  2. Building Docker Images for Static Go Binaries

ThoughtWorks Technology Radar 2016

ThoughtWorks Technology Radar 2016

从这里获取 TECHNOLOGY RADAR NOV '16

无服务器架构

无服务器架构是一种架构方法,使用即时请求、用后即销毁的短暂计算能力来取代长期运行的虚拟机。我们认为这是一种有效的架构选择。需要指出的是,无服务器架构并非一种绝对的架构风格:我们某些团队将系统中的一部分采用无服务器架构,而其它部分继续采用传统架构。

Apache Mesos

我们依然积极使用Apache Mesos管理分布式系统的集群资源。Mesos通过抽象出底层的C96PU和存储等计算资源,从而在保持运行环境隔离的情况下提供良好的资源利用率和效率。Mesos包含了Chronos作为可容错的分布式定时任务执行器,以及Marathon来调度长时间运行在容器中的进程。

Electron

Electron是使用HTML,CSS和JavaScript等Web技术构建本地桌面客户端的实用框架。团队可以充分利用他们开发Web的能力来交付精致的跨平台桌面客户端,而无需花费时间学习另一系列技术。

Grafana

Grafana可以轻松地从不同数据源中创建有用且优雅的监控面板。它有一项很实用的特性,在不同图表中应用同一时间刻度,可以帮助发现数据隐藏的关联关系。模版系统的引入让我们看到了更多的期待,这可以更容易地管理相似的服务。Grafana已成为我们在监控领域的首选。

HashiCorp Vault

项目中的密码管理已成为非常重要的事情。以前是将这些密码放在一个文件或者环境变量中,但这种方式越来越难于管理,特别是在多个应用和多个微服务共享环境的情况下。HashiCorp Vault对这个问题的解决机制是为安全访问密码提供统一的接口。它已经在我们多个项目中很好地解决了这个问题,Vault同HashiCorp服务集成的简单性也深受团队的欢迎。

Terraform

在Terraform的助力下,你可以仅仅通过一些声明式的定义, 来管理云基础设施。通过Terrafrom生成基础设施配置后,通常会用Puppet、Chef或者Ansible这种配置部署工具去实施构建。我们很认可Terraform,因为其语法简单、可读性强, 并且无缝支持多种云服务提供商。

服务器Docker容器化笔记

近期把服务器重新整了下,除了 NGINX 放在物理机上,别的全都容器化。服务器操作系统为Debian。以 Ghost 为例,过程如下:

前提

# HTTPS support for APT
apt-get update  
apt-get install apt-transport-https ca-certificates  

安装 Docker

# Add key and repo
apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D

cat << EOF > /etc/apt/sources.list.d/docker.list  
deb https://apt.dockerproject.org/repo debian-jessie main  
EOF

# Go!
apt-get update  
apt-get install docker-engine  

安装 NGINX

# Add key and repo
wget https://nginx.org/keys/nginx_signing.key && apt-key add nginx_signing.key && rm nginx_signing.key

cat << EOF > /etc/apt/sources.list.d/nginx.list  
deb http://nginx.org/packages/debian/ jessie nginx  
deb-src http://nginx.org/packages/debian/ jessie nginx  
EOF

# Go!
apt-get update  
apt-get install nginx  

启动 Ghost 容器

docker run -d --restart=always --name ghost -p 127.0.0.1:2368:2368 -e NODE_ENV=production -v /root/containers/ghost:/var/lib/ghost ghost  

注意,Ghost 的默认配置有个 Bug,至今为止还没修复,需要手动改一下

I figured it out. Be sure to add this in your config.js to your production property:

paths: {  
    contentPath: path.join(process.env.GHOST_CONTENT, '/')
}

福利,启动 Shadowsocks 服务器

docker run --name shadowsocks --restart=always -d -p 8388:8388 -p 8388:8388/udp -e PASSWORD=$YOUR_PASSWORD vimagick/shadowsocks-libev  

References

  1. Install Docker on Debian - Docker
  2. nginx: Linux packages
  3. How to start Ghost in production mode with this repo/image? #2

在云平台上存储 Secret

翻译自:

TURTLES ALL THE WAY DOWN - Storing Secrets in the Cloud and in the Data Center

Daniel Somerfield

Homepage | YouTube | Slides

引言

乌龟的梗是这么来的:某次霍金发表了一个关于宇宙起源的演讲,有个老太太说,你讲的都是 rubbish,宇宙是一个大乌龟扛着的!有人问,那这个乌龟站在哪里呢?老太太说,你傻叉吗,乌龟站在另一个更大的乌龟上。

这个解释恰好也是 Secret Store 面临的一个问题。数据通过密钥来保护,那密钥通过什么来保护呢?通常我们别无选择,只能再设置一个 Master Key 来加密密钥。更 General 地说,每次我们试图把一个东西弄得安全,都不得不假设假设另一个东西(例如Master Key,信道,etc.)是安全的。

似乎是无解的。那应该怎样做?这就是以下要讨论的。

目标

Security Goals

  • Secrets are secrets Secret 应该以加密的方式存储
  • Auditing 所有的访问和操作都应该被记录,以便出问题时审查
  • No reliance on heroes 不能让整个系统依赖某一个人(比如某个 DBA)
  • Standard practices 遵守某些既定的业界标准,比如协议、算法等。但这不是让你在 Github 上随便下载一个开源工具,就以为他已经身经百战了,Naive!

Operational Goals

  • Automated 可以自动化部署,就像其他的任何组件一样
  • Scales operationally 在数据量慢慢变大以后,也有方法能应对这种变化

Easy to Use

它一定要容易被别的开发者所使用,不然占用的都是自己的时间。这一点对于任何组件的开发同样适用。

第一只乌龟

首先,不管那么多了——Secret 需要是安全的。目标:

  • Secrets 是加密的
  • 受权限控制的分发
  • 自动化地部署到应用程序

策略1: orchestrator decryption

如果你用了 Git-crypt 或者 ansible vault 那你多数是这种情况。

优势:

  • 容易管理 Key,因为是集中存放在 orchestrator
  • 容易和现有的 orchestration 工具集成

缺点:

  • 所有的东西都依赖于 orchestrator,有风险
  • 会导致产生很多不用的 Secret
  • 更多的“乌龟”,Provision可靠吗?传输信道可靠吗?

策略2:application decryption

优势:

  • orchestrator 和 Secret Store 解耦
  • 容易和现有的 orchestration 工具集成

缺点:

  • Key 管理不那么容易
  • 会导致产生很多不用的 Secret
  • 更多的“乌龟”,Provision可靠吗?传输信道可靠吗?

策略3:operational compartmentalization

优势:

  • 清晰的责任划分
  • 容易和现有的 orchestration 工具集成

缺点:

  • 清晰的划分也导致了组织孤岛(organizational silos)
  • 缺少透明性

实现上述策略的工具

1. SCM 加密

加密整个 Source Repo,或者只加密相关的项目。

优点:

  • 容易集成
  • 审计(audit)可以基于 SCM

缺点:

  • 缺少 Secret 滚动功能
  • Data at rest
  • 缺少 usage 信息的 audit
  • 更多的乌龟

相关工具:

  • Blackbox
  • GitCrypt
  • Transcrypt

(未完)

用 Hadoop 统计词频并存入 HBase 中

统计一个 TXT 中的所有词语出现的平均频率(总出现次数/总共出现过的TXT文档数量),并写入 Hbase

一共用到 MapRecuce 的四个步骤:

Mapper 负责把把原来的任务分成很多Key-Value块。本题中,我们把任务分成这样的键值对:<Term#Doc, 1>

public class TokenizerMapper extends Mapper<LongWritable, Text, Text, IntWritable> {

    private static final IntWritable one = new IntWritable(1);
    private Text word = new Text();

    @Override
    public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        StringTokenizer itr = new StringTokenizer(value.toString());
        while (itr.hasMoreTokens()) {
            word.set(itr.nextToken() + '#' + getSource((FileSplit) context.getInputSplit()));
            context.write(word, one);
        }
    }

    private static String getSource(FileSplit split) {
        String fileName = split.getPath().getName();
        return fileName.split("\\.", 2)[0];
    }
}

Combiner 是可选的,负责在 Mapper 后做个简单的合并,从而可以减少 Mapper 节点到 Reducer 节点之间的传输代价。比如对于上述的键值对,如果 Term#Doc 相同(换句话说,同一个词在同一篇文档中出现多次),可以直接替换成 <Term#Doc, n> , n为出现次数

public class LocalSumCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {  
    private IntWritable result = new IntWritable();

    @Override
    public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int sum = 0;
        for (IntWritable val : values) {
            sum += val.get();
        }
        result.set(sum);
        context.write(key, result);
    }
}

Partitioner 负责把 Key-Value 对 shuffle 到某个 Reducer 上。缺省的 Partitioner 就是对 Key 做哈希、再和 Reducer 数量取模。一方面 Partition 能实现 Reducer 的负载均衡,另一方面,我们常常希望这个分配的过程有意义,例如把同一个词(term)分到同一个 Reducer 上。

public class TermPartitioner extends Partitioner<Text, IntWritable> {  
    @Override
    public int getPartition(Text key, IntWritable value, int numReduceTasks) {
        String term = key.toString().split("#")[0];
        return term.hashCode() % numReduceTasks;
    }
}

Reducer 负责汇总数据,产生结果。注意到 MapReduce 框架保证 shuffle 过来的数据是排序过的。

public class WordFreqReducer extends TableReducer<Text, IntWritable, ImmutableBytesWritable> {

    private String prevTerm = "";

    private List<Posting> postings = new ArrayList<>();

    @Override
    public void reduce(Text key, Iterable<IntWritable> values, WordFreqReducer.Context context)
            throws IOException, InterruptedException {
        String keySplits[] = key.toString().split("#");
        String term = keySplits[0];
        String document = keySplits[1];

        if (!term.equals(prevTerm)) {
            cleanup(context);
        }

        int count = 0;
        for (IntWritable val : values) {
            count += val.get();
        }

        postings.add(new Posting(document, count));
        prevTerm = term;
    }

    @Override
    public void cleanup(WordFreqReducer.Context context) throws IOException, InterruptedException {
        if (!"".equals(prevTerm)) {
            int total = postings.stream().mapToInt(Posting::getFrequency).sum();
            double average = (double) total / postings.size();

            Put put = new Put(Bytes.toBytes(prevTerm));
            put.addColumn(Bytes.toBytes(InvertedIndexer.COLUMN_FAMILY_NAME),
                    Bytes.toBytes(InvertedIndexer.COLUMN_NAME), Bytes.toBytes(String.format("%.2f", average)));
            context.write(null, put);
            postings.clear();
        }
    }

    static private class Posting {
        private final String document;
        private final int frequency;

        Posting(String document, int frequency) {
            this.document = document;
            this.frequency = frequency;
        }

        public String getDocument() {
            return document;
        }

        public int getFrequency() {
            return frequency;
        }

        @Override
        public String toString() {
            return String.format("%s:%d", document, frequency);
        }
    }
}

最后附上 main 函数的代码:

public class InvertedIndexer {  
    private static Logger logger = Logger.getLogger(InvertedIndexer.class);

    private static final Configuration HBASE_CONFIG = HBaseConfiguration.create();

    public static final String TABLE_NAME = "wuxia";
    public static final String COLUMN_FAMILY_NAME = "cf";
    public static final String COLUMN_NAME = "freq";

    public static void main(String[] args) throws Exception {
        createTableIfNotExist(TABLE_NAME, COLUMN_FAMILY_NAME);

        Job job = Job.getInstance(new Configuration(), "word_freq");
        job.setJarByClass(InvertedIndexer.class);

        TableMapReduceUtil.initTableReducerJob(TABLE_NAME, WordFreqReducer.class, job, TermPartitioner.class);
        job.setMapperClass(TokenizerMapper.class);
        job.setPartitionerClass(TermPartitioner.class);
        job.setCombinerClass(LocalSumCombiner.class);
        job.setReducerClass(WordFreqReducer.class);

        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);
        FileInputFormat.addInputPath(job, new Path(args[0]));

        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }

    private static void createTableIfNotExist(String tableName, String... families)
            throws Exception {
        Connection connection = ConnectionFactory.createConnection(HBASE_CONFIG);
        Admin admin = connection.getAdmin();

        HTableDescriptor desc = new HTableDescriptor(TableName.valueOf(tableName));
        for (String family : families) {
            desc.addFamily(new HColumnDescriptor(family));
        }

        if (admin.tableExists(TableName.valueOf(tableName))) {
            logger.info(String.format("Table '%s' already created", tableName));
        } else {
            admin.createTable(desc);
            logger.info(String.format("Created table '%s'", tableName));
        }
    }
}

注意,用到 HBase 的情况下,依赖只需要这两个就行了:

<dependency>  
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-client</artifactId>
    <version>1.2.3</version>
</dependency>  
<dependency>  
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-server</artifactId>
    <version>1.2.3</version>
</dependency>  

以及 Apache 源

<repository>  
    <id>apache</id>
    <url>http://maven.apache.org</url>
</repository>