Bowen's Blog

Respect My Authorita.

AWS KMS and Its Usage

| Comments

什么是KMS


KMS是AWS提供的中心化的key托管服务,它使用硬件安全模块 (HSM)保护密钥安全。它可以被集成到其它的AWS服务中,如S3, EBS, RDS等,同时所有关于key的使用都会在CloudTrail中记录,以方便审计。

KMS的优点


基本上来自于文档,其好处有如下几点:

  1. 中心化的key托管服务。举个例子,对于不同的环境(staging/production),我们需要维护不同private key 去做部署,调试等等,还得考虑定期rotate。出于安全考虑,这些private key不推荐和部署的repo放在一起。一般情况下你得把它们放在一个统一的地方去保存,如Rattic或者Vault去管理。这样的话,你的承担这些工具的维护任务。KMS可以让你免除维护的压力。
  2. 和 AWS 服务的集成。S3,EBS,RDS的数据加密,都可以使用KMS。同时,它也支持命令行或者API去管理key,进行key的rotate,加密解密等。
  3. 可伸缩性、耐用性和高可用性。KMS会自动帮你保存key多份拷贝,耐用性99.999999999%,同时KMS会在多个AZ部署,保证高可用性。
  4. 安全。KMS在服务端通过硬件加密,保证了你在上面存储的key的安全性。其实现的细节在这里
  5. 审计。对于key的请求,都会被记录在CloudTrail中,方便审计。

可以看到的好处有很多,比如直接把加密过后的private key或者密码扔到repo中,再也不用担心被别人拿去干坏事。

使用 KMS 服务


要使用KMS服务,首先得创建一个新的master key。key是按照region划分, 自己创建key的价格是1刀一个月,每个月的前20000次请求是免费的。

创建新key


在AWS Console -> IAM界面的Encryption Keys中找到创建Key和Key管理的选项,如key的EnableDisable或者删除等。当然,我们可以通过AWS CLI来创建key,这样可以将整个过程用代码管理起来:

  1. 假设AWS account为123456789,指定key policy并保存到文件(e.g policy.json)中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
  "Id": "KeyPolicy-1",
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Allow access for Admin",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789:root"
      },
      "Action": [
        "kms:Create*",
        "kms:Describe*",
        "kms:Enable*",
        "kms:List*",
        "kms:Put*",
        "kms:Update*",
        "kms:Revoke*",
        "kms:Disable*",
        "kms:Get*",
        "kms:Delete*",
        "kms:ScheduleKeyDeletion",
        "kms:CancelKeyDeletion"
      ],
      "Resource": "*"
    }
  ]
}
  1. 创建key,并绑定对应的policy
1
 ~> aws kms create-key --key-usage "encryption key" --description "master key" --policy "$(cat policy.json)"

返回的内容可能如下

1
2
3
4
5
6
7
8
9
10
11
{
  "KeyMetadata": {
    "KeyId": "aabbccdd-4444-5555-6666-778899001122",
      "Description": "master key",
      "Enabled": true,
      "KeyUsage": "encryption key",
      "CreationDate": 2433401783.841,
      "Arn": "arn:aws:kms:ap-southeast-2:123456789:key/aabbccdd-4444-5555-6666-778899001122",
      "AWSAccountId": "123456789"
  }
}
  1. 授权IAM user/role去使用或者管理key,这是除了policy之外的另一种访问管理控制的机制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "Sid": "Allow use of the key",
  "Effect": "Allow",
  "Principal": {"AWS": [
    "arn:aws:iam::111122223333:user/KMSUser",
    "arn:aws:iam::111122223333:role/KMSRole",
  ]},
  "Action": [
    "kms:Encrypt",
    "kms:Decrypt",
    "kms:ReEncrypt*",
    "kms:GenerateDataKey*",
    "kms:DescribeKey"
  ],
  "Resource": "*"
}
  1. 创建alias,可以作为keyid的替身使用
1
aws kms create-alias --alias-name "alias/test-encryption-key" --target-key-id aabbccdd-4444-5555-6666-778899001122
  1. 使用key去加密文件。加密后的输出为base64编码后的密文,可以进一步解码为二进制文件。
1
aws kms encrypt --key-id 1234abcd-12ab-34cd-56ef-1234567890ab --plaintext fileb://ExamplePlaintextFile --output text --query CiphertextBlob | base64 --decode > ExampleEncryptedFile
  1. 解密文件,原理如加密的过程。
1
aws kms decrypt --ciphertext-blob fileb://ExampleEncryptedFile --output text --query Plaintext | base64 --decode > ExamplePlaintextFile

局限性

这种使用KMS的方式只能加密最多4KB的数据。想要加密更大的数据可以使用KMS去生成一个Data Key,然后利用Data Key去加密数据。

使用场景举例

在REA项目中,在AWS上部署的大多数APP都是(尽量)遵循12factors原则的。应用运行时依赖的配置是通过user-data传入环境变量设置。在一个instance上启动服务的过程大致如下: 1. 在launchConfiguration中为instance添加instanceProfile,对应的role有使用KMS的权限; 2. 在user-data中设置cypher text并且解密到环境变量中:

1
2
3
4
5
6
7
8
9
10
11
cipher="CiBwo3lXT5T+pTZu7P9Cqkh0Iolpaz9FMzha5jJb6kTdiBKNAQEBAgB4cKN5V0+U/qU2buz/QqpIdCKJaWs/RTM4WuYyW+pE3YgAAABkMGIGCSqGSIb3DQEHBqBVMFMCAQAwTgYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAwIxkIN0TeX1HiWyj0CARCAIVaSfD/spTBFAfBVIp/Wy6TadlwUKKz/oTMWUUob9fcxdg=="
cipher_blob=$(mktemp /tmp/blob.123)
echo -n "${cipher}" | base64 -D > cipher_blob
PASSWORD=$(aws kms decrypt --ciphertext-blob fileb://$cipher_blob \
             --query "Plaintext"                         \
             --output text                               \
             --region ap-southeast-2  | \
             base64 -D
)

docker run -d -e PASSWORD=$PASSWORD .......

Stop Wrapping AWS SDK to Create Tools

| Comments

大概4-5年前,客户开始使用AWS作为他们的开发和测试环境,因为澳洲当时没有亚马逊的数据中心,所以只好 使用us-east, us-west这些region。后来澳洲有了AWS的数据中心,应用的产品就都迁移到新的region 和新的AWS账户下。由于这个历史原因,一部分bake AMI的任务以及部署的任务,都是跨账户以及region 的。

这些任务的工具,都是用ruby的aws-sdk包装的,从表面上看,这么做有如下的好处:

  1. 更细粒度去控制这些任务以及过程
  2. 代码可测试
  3. 打包/发布/共享会更加容易些
  4. 只要SDK支持,你都可以用自己熟悉的语言去实现这些工具
  5. 写代码实现感觉很牛逼

对于程序员来说,这么做感觉棒棒哒,写完很有满足感。但是实际中会带来很大的问题,具体表现在维护方面。 我举两个例子:

  1. 不是所有人都喜欢这个工具,有些人会提交patch,改进这个工具,有些人会重新实现一个类似功能的工 具,比如我喜欢用Java,但是现有的工具是用Ruby实现,我表示不服,重头写一个。维护的难度在这种分散 的项目和语言中增大了。
  2. 实现者没有在对工具进行维护,其中依赖的sdk已经过期,而作为工具的使用者,并没有察觉到这件事情, 在实际的使用中会遇到问题,面对非常深的stacktrace,debug的难度较高。

最近,我和同事就碰到了这样的问题。我们AMI构建和部署的工具是用Ruby的aws-sdk实现,我们要做的工 作是把构建AMI和部署的任务从一个AWS Account移到另外一个Account,原本的验证方式是通过Hard Code的Credentials如AWS_SECRET_ACCESS_KEY/AWS_ACCESS_KEY_ID。更好的实践是通过STS服务 ,用AssumeRole的方式去获得临时的Credential。听上去并没有什么太大的难度,但是当我们去迁移的时 候,发现始终提示权限不足,而仔细检查role的权限后发现没有任何不妥,于是百思不得其解,尝试追踪 stacktrace也没什么结果。

无意中看了眼Gemfile,感觉aws-sdk的版本有点低,随手升了个级,然后试了下,竟然可以通过验证 了……。

多花了两个小时,就是因为没有再去维护这个工具。而这个工具实际实现的功能,用aws-cli也可 以很容易实现,而且依赖更少:

  1. 本地使用,只需要有aws-cli(可能还有python,除了windows,一般的系统默认都会有)和bash就可 以了
  2. CI的slave上使用,可以让aws-cli在镜像启动时自动更新,这样就完全不需要维护
  3. 如果cli参数有变动,提示会更加直接些,也容易追踪

所以,如果大家要针对AWS做一些开发,比如镜像构建,清理或者自动化部署,推荐使用CLI的方式,而不是 SDK去实现,从使用的角度,依赖更少,从维护的角度,成本更低。

更加安全和简单的方式通过堡垒机ssh

| Comments

理想情况下的运维,是不需要ops去ssh到服务器上检查问题(包括安全问题)/日志等,这些是可以通过更好 监控,如使用newrelic,或者更好的日志收集系统,如splunk等去避免。不过现实不总是完美的,加上历史 遗留的原因,ops总是会ssh到堡垒机(bastion host),然后跳转到目标服务器去做操作。

于是,就有很多人(包括我)在堡垒机上生成key/pair, 而且private key很少加密(包括我),这个存在 很严重的安全风险。

一个比较合理的方式是通过ssh proxy的方式去访问目标服务器,这样不需要把key暴露给bastion,比如:

1
 ~> ssh -L 3333:destination_host:22 user@bastion

然后再启动一个新的ssh进程去通过proxy连接:

1
 ~> ssh -p 3333 user@0

每次这么操作略麻烦,可以通过在ssh配置文件简化:

1
2
3
4
Host bastion
        HostName 192.168.1.1
        HostKeyAlias bastion
        LocalForward 9999 target:22

那么建立proxy就只是ssh user@bastion就可以了,然后同理去ssh -p 9999 user@0。 这么做的坏处在于~/.ssh/config配置可能会迅速膨胀,同时,每次还是启动两个进程去完成这件事情,不开心。

于是,我们的安全大神介绍一个更加简单的方法,在~/.ssh/config中,加入下面的内容:

1
2
Host */*
        ProxyCommand ssh $(dirname %h) -W $(basename %h):%

如此我就可以通过ssh user@bastion/target的方式直接ssh到远程主机,ProxyCommand指令会 生成两个进程,后台proxy进程,前台的进程直接通过proxy连接到目标主机。这样从命令行窗口看来我只 是打开了一个会话。同时,你可以链接很多个主机,如ssh user@bastion/targetA/targetB/targetC。 依次通过前一个主机建立的proxy连接到后面的主机上。

这个方法有一些局限:

  1. 不能在主机链上指定不同的端口;
  2. 不能对不同的主机使用不同的登录用户名;
  3. 不同链上建立的连接不能重用已经建立的连接,这可能会导致连接的速度减缓;
  4. 其实还有个问题就是不能很容易的从targetC退出到 targetB…… (我想的)

为了解决这些问题,大神想出了终极解决方案:

1
2
3
4
Host */*
    ControlMaster auto
    ControlPath   ~/.ssh/.sessions/%r@%h:%p
    ProxyCommand /bin/sh -c 'mkdir -p -m700 ~/.ssh/.sessions/"%r@$(dirname %h)" && exec ssh -o "ControlMaster auto" -o "ControlPath   ~/.ssh/.sessions/%r@$(dirname %h):%p" -o "ControlPersist 120s" -l %r -p %p $(dirname %h) -W $(basename %h):%p'
  1. Host */*: 匹配ssh到A/B/X这样的主机类型,然后递归的ssh到链中的主机;
  2. ControlMaster auto: 这个指令的意思是指ssh应当复用已有的control channel连接远程主 机,如果这样的channel不存在,则重新创建,以便以后的链接复用;
  3. ControlPath ~/.ssh/.sessions/%r@%h:%p: 这个指令告诉ssh control channel socket 文件的位置。对于每个远程主机,socket文件应该是唯一的,如此我们可以重用已有连接并且跳过验证。所 以我们用%r(remote login name),%h(remote host name)和%p(端口)作为文件名的部分。 唯一的问题是因为路径中的/,这里会在%h被当成一个目录,但是ssh不会自动创建目录;
  4. ProxyCommand blah: 命令开始时就先创建了所有必须的目录。 ControlPersist的意思是如果 control channel 2分钟内没有活动则停止ssh进程。如果你有两个会话bastion/HostAbastion/HostB,如果不配置ControlPersist,结束第一个进程时第二进程也会同时被干掉。

所以,当你用上面的配置去ssh user@bastion/A/B/C时:

  1. ssh 匹配到了*/*模式
  2. ssh 尝试重用~/.ssh/.sessions/user@bastion/A/B/C:22的socket,如果成功则建立连接, 没有则继续执行
  3. ssh执行ProxyCommand中的内容, 创建目录同时递归的ssh到最终的主机C
  4. 然后ssh在主机C上进行身份验证,成功则创建~/.ssh/.sessions/user@bastion/A/B/C:22的 control channel socket文件,并且成为control channel的master
  5. 显示命令行提示符

你现在有没有和我一样晕,在和大神交流一番后,大神告诉我一个改进版的配置:

1
2
3
4
5
6
Host */*
        ControlMaster auto
        ProxyCommand    /usr/bin/ssh -o "ControlMaster auto" -o "ControlPath ~/.ssh/.sessions/%%C" -o "ControlPersist 120s" -l %r -p %p $(dirname %h) -W $(basename %h):%p

Host *
        ControlPath     ~/.ssh/.sessions/%C

这个配置要简单些,不过他假设你已经创建了~/.ssh/.sessions目录。

荣耀归于Dmitry大神,虽然那个ssh keypair我还没有删除……。

Don't Copy/paste Keys From Chatting Tools

| Comments

今天,别的组的同事过来问我一个关于SSH的问题,问题是这样的:

  1. 客户把AWS的ssh instance的private key通过slack拷给了同事;
  2. 同事发现用部署工具fabric可以使用该key,ssh到EC2的instance上进行部署;
  3. 但是如果使用key去ssh(如ssh -i key user@instance)到EC2的instance,就会提示输入passphrase
  4. 客户的Ops很肯定说这个private key没有加passphrase

这个问题很有趣,我先查看了下key,在我的印象里,如果在ssh-keygen的时候加入密码保护了,private key 中会有如下的额外信息:

1
2
3
4
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,B88893260B6CCFDC6304101075B74A9F
.....

但是同事给我的private key中没有.

在不同的系统下尝试用该key去ssh到EC2 instance得到的结果都是需要输入passpharse,通过输入冗余 ssh信息ssh -vvvv也没有看到什么有用信息(其实是我忽略了)。

google搜索,猜测会不会是key generate时候的格式不同导致的,但是觉得这种可能性不高。

在客户的ops channel询问了下,有人给出了这个建议,用

1
openssl rsa -text -noout -in KEYFILE

去检查key的完整性,返回结果如下:

1
2
3
[vagrant@localhost ~]$ openssl rsa -text -noout -in id_rsa
unable to load Private Key
140516793460640:error:0906D066:PEM routines:PEM_read_bio:bad end line:pem_lib.c:802:

实际上这个信息已经比较明显了,另外有人也从ssh debug的信息中指出:

1
2
debug1: key_parse_private2: missing begin marker
debug1: key_parse_private_pem: PEM_read_PrivateKey failed

private key的开始或者结束的marker出问题了,于是客户询问这个key是不是从slack拷贝过去的,因为 聊天工具有时候会自动纠错,把结束的marker ----自动改成——,他曾经就遇到过这种情况。 再看一遍private key,果然是这样……好羞愧。修改后,果然可以顺利ssh 到instance上了。 (更正下,虽然他指出了问题的来源,但是这段debug信息,在private key是完整的情况下仍然存在,所以这不是key出错的绝对证据。)

从这个事情中,我们可以得到一些教训

  1. 不要用聊天工具copy/paste private key或者代码之类的东西,很容易引起错误。
  2. 同时,这种方式也很不安全,尽量不要这么做,要么在传递完后迅速删除聊天记录
  3. 或者使用一个公共的key管理的服务,如rattic,或者使用临时生成的credential来ssh,如这个项目进一步提高安全性。

Make Everything Production Like - (2/2)

| Comments

开发环境出问题的时候,影响到只是自己,如果持续集成环境或者其相关的基础设施出了问题,那影响到的就 是所有人以及整个开发的进展,我们曾经遇到一次这样的事故,整个 Bamboo(CI)环境的Master和Database都被干掉了,出乎意料的是AWS RDS的自动镜像同时也被删除,于是所有的人花了一个礼拜才重新建好了全部的流水线。

除此之外,一些基础设施,比如企业私有的Repository(如Nexus, Koji, rubygems服务器等)出现问题,也会影响到整个开发和持续交付的时间。

如何解决这个问题?很简单,提高这些环境的可用性,把他们当做产品环境一样看待,提高出错的响应速度, 减少平均恢复时间等。

先举一个CI环境当做产品环境来对待的例子。 一些简单的背景:

  1. 客户使用的持续集成工具是Bamboo
  2. CI Master,Agent以及数据库服务都采用了AWS的服务,如EC2、RDS、R53等
  3. 用CloudFormation去管理整个CI服务的基础设施,同时用Rake task去简化管理的难度。

其具体的结构图如下: arch

该结构详细解释如下:

  1. Bamboo Agent和 Bamboo Master的依赖及其配置打包成RPM,部署的EC2 instance基于Centos定制过的AMI
  2. Bamboo Master/Agent/DB 都用CloudFormation管理
  3. 在Bamboo Agent Stack的LaunchConfiguration中的Metadata中,安装在Agent中运行各种build的依赖, 比如不同的Ruby版本等,同时定义cfn-hup服务,监听Agent的Stack变化,如果有Metadata的变化, 比如,更新了Agent上支持的Java版本,则在Agent上更新该配置
  4. Bamboo Agent由一个AutoScalingGroup管理,除了自动Scale,还可以每天定时启动或者停止Agent Instance,节省成本
  5. Bamboo Master的Stack中做的事情类似
  6. Bamboo Master的SecurityGroup只接受来自Bamboo Agent的SecurityGroup的访问,Bamboo Master DB的SecurityGroup只接受来自Bamboo Master SecurityGroup的请求
  7. Bamboo Master DB使用RDS服务
  8. Bamboo Master服务器上运行的Cron Job每天会定时备份文件系统的Snapshot
  9. Bamboo 服务器上的一个Plan每天会运行定时的任务,创建Master DB的Snapshot,RDS可以设置自动 生成snapshot,不过一旦Master DB被干掉,snapshot也会被一起干掉。所以,安全期间,还是manual snapshot比较好。

回顾这套结构,如果某个Agent挂掉,AutoScalingGroup会重新spin up一个新的Agent Instance。 如果Bamboo Master或者Master DB挂掉,也可以通过CloudFormation Stack以及备份的Snapshot 在1-2个小时以内恢复,时间的开销相对较少。

仔细的同学可能会注意到,为了满足运行build的各种条件,需要安装各种依赖,比如不同的Ruby版本, 不同的Java版本等,重新创建一个Agent Instance到配置完成注册成为Bamboo服务,时间会比较长。而且 如果Metadata的更新导致环境失败,会迅速影响到所有的Agent。

相信很多人会想到更好的解决方案,比如将每个build任务都在Docker容器中运行,如此作为整个CI环境 的维护者,只需要保证每个Agent上面有docker deamon运行,整个Agent挂掉的几率大大降低,同时维护 的责任分散到每个团队内,减轻了维护的压力。

下面介绍如何提高企业内部的私有Repository,如Nexus的可用性和稳定性以及快速恢复能力。 我们的Nexus服务器的结构图,如下: nexus arch

详细解释如下:

  1. Nexus服务运行在ELB后的一个EC2 Instance上
  2. 其部署基于安装有Nexus服务的Base AMI以及CloudFormation stack
  3. Nexus的artifact目录挂载在一个EBS volume下,Instance在初始化时配置了InstanceProfile, 在crontab添加脚本,可以用InstanceProfile中的role去创建EBS volume的daily snapshot,以防止artifact数据丢失
  4. 监控方面,如果ELB下面的健康的Instance数量少于1或者Instance上的EBS Volume没有正确的挂载,都会触发Cloudwatch Alarm,并通过SNS通知Pagerduty,然后Pagerduty再将警报发给维护Nexus的Ops

对于上面的Nexus结构,由于有足够的备份,不论是Volume挂载失败需要恢复或者是Instance当机,处理的 时间成本都会比较低,在半个小时以内。

开发/测试依赖的环境可能还有很多,更多的把它们当做产品环境对待,会大大增加持续交付的流畅度,减轻环境维护方面的痛楚。

Specifying Amazon Credentials in Packer

| Comments

Packer是一个用一份配置构建跨平台镜像的工具,它支持EC2 AMI,Vmware, QEMU,Virtualbox,docker等多个平台。

我们使用AWS作为基础设施平台,通过EC2 AMI作为镜像部署,构建AMI工具基于Packer,加上了一些定制 的东西以减少配置,简化使用的方式。

Packer在构建AMI时,需要创建临时的key pair,security group以及EC2实例,因为需要对应的API credentials,像这样:

1
2
access key id:     AKIAIOSFODNN7EXAMPLE
secret access key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

所以Packer需要在配置文件中hard code credentials或者通过环境变量传入,如果这些都没有找到, Packer会通过如下的步骤去自动查找credential:

1.查找AWS相关环境变量,如:

1
2
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

2.寻找AWS配置文件~/.aws/credentials或者AWS_PROFILE环境变量

3.查找运行Packer的EC2实例中instance profile中的role,然后用AWS STS(Security Token Service)来获取临时的credential.该role的policy至少要允许如下的Actions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
  "Statement": [{
      "Effect": "Allow",
      "Action" : [
        "ec2:AttachVolume",
        "ec2:CreateVolume",
        "ec2:DeleteVolume",
        "ec2:CreateKeypair",
        "ec2:DeleteKeypair",
        "ec2:CreateSecurityGroup",
        "ec2:DeleteSecurityGroup",
        "ec2:AuthorizeSecurityGroupIngress",
        "ec2:CreateImage",
        "ec2:RunInstances",
        "ec2:TerminateInstances",
        "ec2:StopInstances",
        "ec2:DescribeVolumes",
        "ec2:DetachVolume",
        "ec2:DescribeInstances",
        "ec2:CreateSnapshot",
        "ec2:DeleteSnapshot",
        "ec2:DescribeSnapshots",
        "ec2:DescribeImages",
        "ec2:RegisterImage",
        "ec2:CreateTags",
        "ec2:ModifyImageAttribute"
      ],
      "Resource" : "*"
  }]
}

这几种方式中,使用instance profile的的方式是最好的,原因大致如下:

  1. 固定credential的方式会带来安全隐患,一旦泄露,会导致巨大的损失。
  2. 从安全的角度考虑,API credential需要rotate,这样会带来维护成本。读取配置文件也有同样的 问题。
  3. 在持续集成流水线AMI构建任务中,credential必须通过环境变量的方式传入,任务本身增加了额外的 依赖,维护成本提高。

之所以总结Packer在构建AMI时指定credential的方式,是因为最近在把一个用packer构建AMI的流水线 从一个Region移到另一个Region时遇到了原有的credential不工作的情况,后来发现,其实运行这个任务 的Agent(EC2实例)初始化时就绑定了instance profile,不用设置任何额外的credential,packer可以 自动去读取,于是在移除了原有的各种credential之后,顺利的构建出了镜像。

Solution for Joseph Question

| Comments

囧瑟夫问题:

描述

有n只猴子,按顺时针方向围成一圈选大王(编号从1到n),从第1号开始报数,一直数到m,数到m的猴子退出圈外,剩下的猴子再接着从1开始报数。就这样,直到圈内只剩下一只猴子时,这个猴子就是猴王,编程求输入n,m后,输出最后猴王的编号。

输入

每行是用空格分开的两个整数,第一个是 n, 第二个是 m ( 0 < m,n <=300)。最后一行是:

1
0 0

输出

对于每行输入数据(最后一行除外),输出数据也是一行,即最后猴王的编号

样例输入

1
2
3
4
6 2
12 4
8 3
0 0

样例输出

1
2
3
5
1
7

有用类似链表的实现方法,我没有这么做……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//for joseph problem
#include <iostream>
using namespace std;

int joseph(int n, int m){
  int flat[300];
  for(int i = 0; i < n; i++)
    flat[i] = 1;

  for(int i = 0, count = n, mod = m - 1; count != 1; i = ((++i) % n)){
      if(flat[i] == 1){
        if(mod == 0){
          flat[i] = 0;
          count--;
        }
        mod = (mod - 1) % m;
      }
  }
  for(int i = 0; i < n; i++){
    if(flat[i] == 1)
      return i + 1;
  }
}

int main(){
  int set_n[100], set_m[100];
  int count = 0;

  while(true){
    int n, m;
    cin >> n >> m;
    if ((n == 0) && (m == 0)){
      break;
    } else {
      set_n[count] = n;
      set_m[count] = m;
      count++;
    }
  }

  for(int i = 0; i < count; i++)
    cout << joseph(set_n[i], set_m[i]) << endl;

  return 0;
}

Deploy Netty Example App to Heroku

| Comments

作为一名java的弱鸡,为了在公共的服务器上重现netty的一个bug,我也是很努力的自己setup了一个简单的netty的java工程,然后计划 部署在heroku上面,这样在github上提交issue的说服力更强一些。

过程


准备netty的项目


比较简单,就是在netty的代码库中抄一些例子就可以了.要注意的有下面几点:

  1. 申明对netty的依赖,如下:
1
2
3
4
5
6
7
<dependencies>
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.0.24.Final</version>
    </dependency>
</dependencies>
  1. 将依赖的netty jar包copy到target目录下:
1
2
3
4
5
6
7
<executions>
    <execution>
        <id>copy-dependencies</id>
        <phase>package</phase>
        <goals><goal>copy-dependencies</goal></goals>
    </execution>
</executions>
  1. 声明应用启动时的main class:
1
2
3
4
5
6
7
<configuration>
    <archive>
        <manifest>
            <mainClass>com.tw.httpserver.HttpHelloWorldServer</mainClass>
        </manifest>
    </archive>
</configuration>
  1. app的启动端口需要去读取heroku预设的PORT随机端口的环境变量,否则前面的代理服务器无法绑定到后端的netty. 我个人觉得这是一个非常坑爹的设定,同时在官方的文档中没有特别说明。很容易引发下面的错误:
1
Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch

解决的办法就是这样

1
static final int PORT = Integer.parseInt(System.getenv("PORT"));
  1. 声明启动web服务所需命令,在Procfile中加入以下内容:
1
web: java  $JAVA_OPTS -cp target/classes:target/dependency/* com.tw.httpserver.HttpHelloWorldServer
  1. 声明系统运行的环境依赖,在 system.properties加入:
1
java.runtime.version=1.7

本地测试


mvn clean install打包到target目录下,然后运行heroku local web,测试应用在本地是否工作正常。

发布和部署


  1. heroku create命令新建一个应用。
  2. 本地提交,并将代码提交到heroku,git push heroku master
  3. heroku ps:scale web=1保证至少有一个实例在运行这个应用。
  4. heroku open可以打开部署后的服务页面。

结论


可能是因为java水平太差,感觉在heroku上部署java应用比部署ruby应用要难一些。部署完成后,发现还是不 能重现bug,感觉好受伤。

Tracing and Production Bug About Netty

| Comments

CTO在slack上给我们通报了一个bug,具体表现为网站请求的一个服务,在接受完请求后,再发起新请求可能会 返回504.

客户的tech lead和我一起追踪这个问题,出问题的是一个微服务,其架构如下:

1
Akamai -> ELB -> Instances -> Netty App(基于unfilterd-netty-server 0.8.4)

当时我的猜测是ELB和instance之间的连接或者处理出了什么问题,但是Tech lead先查看了ELB Cloudwatch 上的错误数量,并不多,Splunk中搜索ELB access log发生的频率也不高,当然从newrelic可以看到应用本 身的rpm也不是很高。

他先做的事情是在产品环境稳定重现bug,手段是通过一个超大header的Get请求,之后再接若干个请求,就可以 复现。CTO认为可能是Akamai和AWS中间发生了什么错误,于是我们查看了Akamai的access log,寻找504, 数量和ELB上出现的错误基本一致,排除是Akamai出问题的可能。

在这个过程当中,他新建了一个github issue,并将分析过程,以及检验的证据comment到issue中,很不错的 实践。

于是问题又回到了ELB和instance之间,我在猜测是不是因为ELB的cross zone负载均衡出了问题,ELB和instance 之间的网络访问出了错,不过回头想想感觉不太可能。查看了instance上access log,发现没有任何5xx的错误 代码记录。

tech lead再次查看了下Splunk,发现在某个时间段后,504的错误突然大幅增加,并且数量、频率比较稳定。 查看CI以及提交记录,发现刚好是一次大的重构的开始时间,将代码改为Freemonad风格,同时引入了unfilterd-netty-server 0.8.4 但是理论上来说重构没有功能性修改,而且也很难判断哪些代码会引起问题。

于是我将staging的版本回退到重构前,测试竟然是好的。。。damn it。

我突然想起来,ELB有个选项,idle connection timeout,大概意思就是说ELB和instance上app间如果 有连接空闲,超过一定时间后才关闭,这是为了减少http通信的开销。很有可能第一次的请求处理后,因为某种原因, 复用的连接没有正常的被Netty服务。所以我们测试了下,先发一次请求,成功,紧接着后面的请求理论上会失败, 因为它复用了前一个ELB和instance的连接,但如果这时再发一次请求,它应该也会成功,因为前一个空闲连接 被占用了。测试了下果然如此,并且当超时时间设置为1s时,基本不会出错了。

所以基本上可以认为是Netty处理大headers的问题,另外一个同事给出了这个netty issue的链接netty#3379,同时它给出了在本地测试的方法,就是用 curl去同时请求两个链接,这个时候它会复用连接,而不是关闭连接再重新打开。果然,unfilterd-netty-server 依赖的netty4.0.24.Final是存在问题的,而比较新的4.0.29.Final是没有问题的。

暴力覆写netty的版本可能会引入新的问题,不过我们试着冒下险,更改后所有测试都通过,并生成新的AMI,然后 在Staging部署测试,一切正常,最后部署产品环境,通知CTO,问题修复。

我们的做法比较粗暴,而且不是好的解决办法,最合适的方法是给unfilterd-netty-server项目发pull request升级它依赖的netty版本号。不过,花了好长时间也没有找到合适的公共netty站点去证实,加上开源 社区的反馈不一定会很快,所以还是选择了先暴力升级的方法。

鉴于还有其他项目使用这个类库,而它们都会存在相同的问题,所以必须在github中找到这些项目。幸好前一段 时间都在做patch management,通过一些插件可以将项目类库的依赖生成json文件并上传到s3 bucket。通过 查找这些依赖的json文件我很快就定位到了需要修改的系统并发了github issue,这些团队会自己决定如何处理 这个问题。

通过解决这次的bug,学到了很多追踪、分析和解决问题的方式,受益匪浅。