目前,弹性伸缩服务已经接入了负载均衡(SLB)、云数据库RDS 等云产品,但是暂未接入 云数据库Redis,有时候我们可能会需要弹性伸缩服务在扩缩容的时候自动将扩缩容涉及到的 ECS 实例私网 IP 添加到 Redis 白名单或者从 Redis 白名单中移除。本文将给出上述场景的最佳实践,向您介绍如何通过 AutoSclaing -> LifecycleHook -> MNS -> FC 的方式实现伸缩组发生扩容时自动将扩容出来的 ECS 实例私网 IP 添加到 Redis 白名单中,您可以在此基础上,根据您的业务需求进行扩展。
函数计算(FC)简介
阿里云函数计算是事件驱动的全托管计算服务。通过函数计算,您无需管理服务器等基础设施,只需编写代码并上传。函数计算会为您准备好计算资源,以弹性、可靠的方式运行您的代码,并提供日志查询、性能监控、报警等功能。借助于函数计算,您可以快速构建任何类型的应用和服务,无需管理和运维。而且,您只需要为代码实际运行所消耗的资源付费,代码未运行则不产生费用。更多关于函数计算的相关信息,您可以通过 函数计算官方文档 进行了解。
消息服务(MNS)简介
阿里云消息服务(Message Service,简称 MNS)是一种高效、可靠、安全、便捷、可弹性扩展的分布式消息服务。MNS能够帮助应用开发者在他们应用的分布式组件上自由的传递数据、通知消息,构建松耦合系统。更多关于消息服务的相关信息,您可以通过 消息服务官方文档 进行了解。
最佳实践
前提条件
在进行以下操作前,您需要先开通 函数计算服务FC 、 消息服务MNS 、弹性伸缩服务AutoScaling,接下来配置我们需要用的 FC、MNS、AutoScaling 相关信息
配置 MNS
登录 MNS控制台,创建 MNS 主题(作为函数计算的触发器),如下图所示:
同样的,创建 MNS 队列,MNS 队列作为函数计算执行结果接收器,队列名称会在代码中进行配置。
配置 FC
登录FC控制台,新建服务,如下图所示:
服务创建好以后,新增函数,如下图所示:
点击新增函数,弹出新建函数对话框,如下图所示:
选择函数语言,并选择空白模板,跳转到触发器配置界面,如下图所示:
配置好触发器类型、触发器名称以及对应的 MNS 主题(MNS 主题与 FC 所属的地域最好相同),点击下一步,跳转到基础管理配置界面,如下图所示:
所在服务默认会选择当前服务,不用改变,填写函数名称,选择运行环境,通过代码包上传的方式上传提前测试好的 java jar包(即触发函数计算时需要执行的运行的程序,本文最后会给出示例jar包),按照说明填写好函数入口,点击下一步,跳转到模版授权管理界面,如下图所示:
首先授予函数运行所需要的权限,授权时候应遵循权限最小化原则,防止权限过大,如上图步骤1、2所示,再授予 MNS 触发 FC 所需的权限,如上图步骤3、4所示,最后点击下一步,跳转到信息核对界面,如下图所示:
核对信息无误,点击创建,函数创建完成。
关于函数计算的配置过程,您可以通过 FC Hello World示例 进行了解。
创建云数据库 Redis
登录 Redis控制台,选择和 MNS 、FC 相同的地域,创建 Redis 实例。实例创建完以后,查看实例的白名单设置,如下图所示:
配置 AutoScaling
登录 弹性伸缩控制台,创建好伸缩组以及伸缩配置以后,创建生命周期挂钩(LifecycleHook),如下图所示:
上图中,在左侧导航栏选择生命周期挂钩,点击创建生命周期挂钩按钮,填写名称,选择生命周期挂钩对应的伸缩活动类型,配置生命周期挂钩对应的 MNS 通知为 MNS 主题,并且选择的主题为 FC 触发器对应的主题,最后点击创建按钮,生命周期挂钩函数创建完成,如下图所示:
在伸缩组发生扩容伸缩活动时,实例创建完成并运行起来以后,生命周期挂钩会被触发,并发送伸缩活动相关信息到生命周期挂钩配置的 MNS 主题上,挂起当前的伸缩活动,直到生命周期挂钩超时或者被提前结束。生命周期挂钩活动结束以后,伸缩活动继续执行,扩容出来的 ECS 实例会被挂载到负载均衡实例上(如果伸缩组配置了负载均衡实例的话)。关于生命周期挂钩功能的详细说明,您可以通过云栖博客 AutoScaling 生命周期挂钩功能 进行详细了解。
触发扩容伸缩活动
首先,我们通过触发扩容伸缩活动的方式,创建 10 台 ECS 实例,对应的伸缩活动如下图所示:
然后我们登录 MNS控制台,查看队列接收到的 FC 执行结果消息,如下图所示:
上述消息中 success 为 true,表示函数计算执行成功(即 ECS 实例私网 IP 添加到 Redis 白名单成功),消息体中还包括了当前生命周期挂钩活动对应的 LifecycleHookId LifecycleActionToken 参数信息,您可以根据相关参数信息调用 CompleteLifecycleAction 接口提前结束生命周期活动。
最后,我们登录 云数据库Redis控制台,查看当前的 Redis 白名单信息,如下图所示:
从上图可以看出,弹性伸缩扩容活动创建出来的 ECS 实例私网 IP 成功添加到 Redis 白名单中。
至此,通过 AutoScaling -> LifecycleHook -> MNS -> FC 实现 Redis 白名单自动添加的过程结束,整体过程如下:
- 弹性伸缩组触发扩容伸缩活动,扩容 ECS 实例,扩容活动触发生命周期挂钩
- 生命周期挂钩将扩容活动挂起,同时发送消息到 MNS 主题
- MNS 主题接收到消息以后将消息作为输入信息触发 FC,FC 被触发以后执行预置业的 JAVA 函数
- JAVA 函数获取 FC 触发器的输入信息,信息中包括了本次伸缩活动对应的 ECS 实例 ID信息,通过接口获取 ECS 实例私网 IP 以后添加到 Redis default 分组白名单中
- 最后,函数执行结果发送到代码中配置好的 MNS 队列中
上述过程仅作为一个参考的 Demo,进一步实现自动化管理,还需要我们自己编程实现,如编程的方式消费 MNS 队列中的消息,获取执行结果与 LifecycleHookId LifecycleActionToken等参数信息提前结束生命周期挂钩活动等。
FC 预置 JAVA 代码解析
FC 预置函数为 JAVA 代码,通过 Maven 管理,对应的代码及依赖如下:
Example.java
package fc;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.aliyun.fc.runtime.Context;
import com.aliyun.fc.runtime.StreamRequestHandler;
import com.aliyun.mns.client.CloudAccount;
import com.aliyun.mns.client.CloudQueue;
import com.aliyun.mns.client.MNSClient;
import com.aliyun.mns.model.Message;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.ecs.model.v20140526.DescribeInstancesRequest;
import com.aliyuncs.ecs.model.v20140526.DescribeInstancesResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.aliyuncs.r_kvstore.model.v20150101.DescribeSecurityIpsRequest;
import com.aliyuncs.r_kvstore.model.v20150101.DescribeSecurityIpsResponse;
import com.aliyuncs.r_kvstore.model.v20150101.ModifySecurityIpsRequest;
import model.FCResult;
import model.HookModel;
import model.MnsMessageModel;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.springframework.util.CollectionUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Example implements StreamRequestHandler {
/**
* 专有网络类型,此参数不用变
*/
private static final String VPC_NETWORK = "vpc";
private static final String CHAR_SET = "UTF-8";
/**
* 接收input数组大小,4096通常够用
*/
private static final Integer MAX_BYTE_LENGTH = 4096;
/**
* REDIS 白名单默认分组
*/
private static final String DEFAULT_SECURITY_GROUP_NAME = "default";
/**
* REDIS 修改白名单的模式
*/
private static final String MODIFY_MODE_APPEND = "Append";
/**
* MNS 客户端发送消息地址
*/
private static final String MNS_END_POINT = "http://%s.mns.%s.aliyuncs.com/";
/**
* 待添加的REDIS实例ID,根据个人情况替换
*/
private static final String REDIS_ID = "";
/**
* 接收本次函数计算执行结果的队列名称,根据个人情况替换
*/
private static final String QUEUE_NAME = "wujin-fc-callback";
/**
* 阿里云账号UID,根据跟人情况替换
*/
private static final Long USER_ID = 1111111111111111111L;
/**
* 伸缩组 MNS FC 所属的region,根据个人情况替换
*/
private static final String REGION_ID = "cn-hangzhou";
@Override
public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) {
FCResult result = new FCResult();
String akId = context.getExecutionCredentials().getAccessKeyId();
String akSecret = context.getExecutionCredentials().getAccessKeySecret();
String securityToken = context.getExecutionCredentials().getSecurityToken();
try {
//获取MNS触发函数计算时输入的内容
String input = readInput(inputStream);
MnsMessageModel mnsMessageModel = JSON.parseObject(input,
new TypeReference<MnsMessageModel>() {
});
if (mnsMessageModel == null) {
result.setSuccess(false);
result.setMessage("mnsMessageModel is null");
sendMns(akId, akSecret, securityToken, result.toString());
return;
}
HookModel contentModel = mnsMessageModel.getContent();
if (contentModel == null) {
result.setSuccess(false);
result.setMessage("contentModel is null");
sendMns(akId, akSecret, securityToken, result.toString());
return;
}
IAcsClient client = buildClient(akId, akSecret, securityToken);
//获取本次伸缩活动对应实例的私网IP
List<String> privateIps = getInstancesPrivateIps(contentModel.getInstanceIds(), client);
if (CollectionUtils.isEmpty(privateIps)) {
result.setSuccess(false);
result.setMessage("privateIps is empty");
sendMns(akId, akSecret, securityToken, result.toString());
return;
}
List<String> needAppendIps = filterPrivateIpsForAppend(privateIps, client);
if (!CollectionUtils.isEmpty(needAppendIps)) {
modifySecurityIps(client, needAppendIps);
result.setLifecycleHookId(contentModel.getLifecycleHookId());
result.setLifecycleActionToken(contentModel.getLifecycleActionToken());
sendMns(akId, akSecret, securityToken, result.toString());
}
} catch (Exception ex) {
result.setSuccess(false);
result.setMessage(ex.getMessage());
sendMns(akId, akSecret, securityToken, result.toString());
}
}
/**
* 构建请求 ECS Redis 接口客户端
*
* @param akId
* @param akSecret
* @param securityToken
* @return
*/
private IAcsClient buildClient(String akId, String akSecret, String securityToken) {
IClientProfile clientProfile = DefaultProfile.getProfile(REGION_ID, akId, akSecret,
securityToken);
return new DefaultAcsClient(clientProfile);
}
/**
* 将执行结果发送消息到MNS
*
* @param ak
* @param aks
* @param securityToken
* @param msg
*/
private void sendMns(String ak, String aks, String securityToken, String msg) {
MNSClient client = null;
try {
CloudAccount account = new CloudAccount(ak, aks,
String.format(MNS_END_POINT, USER_ID, REGION_ID), securityToken);
client = account.getMNSClient();
CloudQueue queue = client.getQueueRef(QUEUE_NAME);
Message message = new Message();
message.setMessageBody(msg);
queue.putMessage(message);
} finally {
if (client != null) {
client.close();
}
}
}
/**
* 过滤出需要添加到redis的私网IP
*
* @param privateIps 过滤以前的私网IP
* @param client
* @return
* @throws ClientException
*/
private List<String> filterPrivateIpsForAppend(List<String> privateIps, IAcsClient client)
throws ClientException {
List<String> needAppendIps = new ArrayList<>();
if (CollectionUtils.isEmpty(privateIps)) {
return needAppendIps;
}
DescribeSecurityIpsRequest request = new DescribeSecurityIpsRequest();
request.setInstanceId(REDIS_ID);
DescribeSecurityIpsResponse response = client.getAcsResponse(request);
List<DescribeSecurityIpsResponse.SecurityIpGroup> securityIpGroups = response
.getSecurityIpGroups();
if (CollectionUtils.isEmpty(securityIpGroups)) {
return privateIps;
}
for (DescribeSecurityIpsResponse.SecurityIpGroup securityIpGroup : securityIpGroups) {
if (!securityIpGroup.getSecurityIpGroupName().equals(DEFAULT_SECURITY_GROUP_NAME)) {
continue;
}
String securityIps = securityIpGroup.getSecurityIpList();
if (securityIps == null) {
continue;
}
String[] securityIpList = securityIps.split(",");
List<String> existIps = Arrays.asList(securityIpList);
if (CollectionUtils.isEmpty(existIps)) {
continue;
}
for (String ip : privateIps) {
if (!existIps.contains(ip)) {
needAppendIps.add(ip);
}
}
}
return privateIps;
}
/**
* 修改REDIS实例DEFAULT分组私网IP白名单
*
* @param client
* @param needAppendIps
* @throws ClientException
*/
private void modifySecurityIps(IAcsClient client, List<String> needAppendIps)
throws ClientException {
if (CollectionUtils.isEmpty(needAppendIps)) {
return;
}
ModifySecurityIpsRequest request = new ModifySecurityIpsRequest();
request.setInstanceId(REDIS_ID);
String ip = StringUtils.join(needAppendIps.toArray(), ",");
request.setSecurityIps(ip);
request.setSecurityIpGroupName(DEFAULT_SECURITY_GROUP_NAME);
request.setModifyMode(MODIFY_MODE_APPEND);
client.getAcsResponse(request);
}
/**
* 获取输入,并base64解码
*
* @param inputStream
* @return
* @throws IOException
*/
private String readInput(InputStream inputStream) throws IOException {
try {
byte[] bytes = new byte[MAX_BYTE_LENGTH];
int tmp;
int len = 0;
//循环读取所有内容
while ((tmp = inputStream.read()) != -1 && len < MAX_BYTE_LENGTH) {
bytes[len] = (byte) tmp;
len++;
}
inputStream.close();
byte[] act = new byte[len];
System.arraycopy(bytes, 0, act, 0, len);
return new String(Base64.decodeBase64(act), CHAR_SET);
} finally {
inputStream.close();
}
}
/**
* 获取实例列表对应的私网IP,并限制每次请求实例数量不超过100
*
* @param instanceIds 实例列表
* @param client 请求客户端
* @return
* @throws Exception
*/
public List<String> getInstancesPrivateIps(List<String> instanceIds, IAcsClient client)
throws Exception {
List<String> privateIps = new ArrayList<>();
if (CollectionUtils.isEmpty(instanceIds)) {
return privateIps;
}
int size = instanceIds.size();
int queryNumberPerTime = 100;
int batchCount = (int) Math.ceil((float) size / (float) queryNumberPerTime);
//support 100 instance
for (int i = 1; i <= batchCount; i++) {
int fromIndex = queryNumberPerTime * (i - 1);
int toIndex = Math.min(queryNumberPerTime * i, size);
List<String> subList = instanceIds.subList(fromIndex, toIndex);
DescribeInstancesRequest request = new DescribeInstancesRequest();
request.setInstanceIds(JSON.toJSONString(subList));
DescribeInstancesResponse response = client.getAcsResponse(request);
List<DescribeInstancesResponse.Instance> instances = response.getInstances();
if (CollectionUtils.isEmpty(instances)) {
continue;
}
for (DescribeInstancesResponse.Instance instance : instances) {
String privateIp = getPrivateIp(instance);
if (privateIp != null) {
privateIps.add(privateIp);
}
}
}
return privateIps;
}
/**
* 从 DescribeInstancesResponse.Instance 中解析出私网 IP
*
* @param instance DescribeInstancesResponse.Instance
*/
private String getPrivateIp(DescribeInstancesResponse.Instance instance) {
String privateIp = null;
if (VPC_NETWORK.equalsIgnoreCase(instance.getInstanceNetworkType())) {
DescribeInstancesResponse.Instance.VpcAttributes vpcAttributes = instance
.getVpcAttributes();
if (vpcAttributes != null) {
List<String> privateIpAddress = vpcAttributes.getPrivateIpAddress();
if (!CollectionUtils.isEmpty(privateIpAddress)) {
privateIp = privateIpAddress.get(0);
}
}
} else {
List<String> innerIpAddress = instance.getInnerIpAddress();
if (!CollectionUtils.isEmpty(innerIpAddress)) {
privateIp = innerIpAddress.get(0);
}
}
return privateIp;
}
}
代码中涉及到的 Model 文件
FCResult.java
package model;
import com.alibaba.fastjson.JSON;
public class FCResult {
private boolean success = true;
private String lifecycleHookId;
private String lifecycleActionToken;
private String message;
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getLifecycleHookId() {
return lifecycleHookId;
}
public void setLifecycleHookId(String lifecycleHookId) {
this.lifecycleHookId = lifecycleHookId;
}
public String getLifecycleActionToken() {
return lifecycleActionToken;
}
public void setLifecycleActionToken(String lifecycleActionToken) {
this.lifecycleActionToken = lifecycleActionToken;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public String toString() {
return JSON.toJSONString(this);
}
}
HookModel.java
package model;
import java.util.List;
public class HookModel {
private String lifecycleHookId;
private String lifecycleActionToken;
private String lifecycleHookName;
private String scalingGroupId;
private String scalingGroupName;
private String lifecycleTransition;
private String defaultResult;
private String requestId;
private String scalingActivityId;
private List<String> instanceIds;
public String getLifecycleHookId() {
return lifecycleHookId;
}
public void setLifecycleHookId(String lifecycleHookId) {
this.lifecycleHookId = lifecycleHookId;
}
public String getLifecycleActionToken() {
return lifecycleActionToken;
}
public void setLifecycleActionToken(String lifecycleActionToken) {
this.lifecycleActionToken = lifecycleActionToken;
}
public String getLifecycleHookName() {
return lifecycleHookName;
}
public void setLifecycleHookName(String lifecycleHookName) {
this.lifecycleHookName = lifecycleHookName;
}
public String getScalingGroupId() {
return scalingGroupId;
}
public void setScalingGroupId(String scalingGroupId) {
this.scalingGroupId = scalingGroupId;
}
public String getScalingGroupName() {
return scalingGroupName;
}
public void setScalingGroupName(String scalingGroupName) {
this.scalingGroupName = scalingGroupName;
}
public String getLifecycleTransition() {
return lifecycleTransition;
}
public void setLifecycleTransition(String lifecycleTransition) {
this.lifecycleTransition = lifecycleTransition;
}
public String getDefaultResult() {
return defaultResult;
}
public void setDefaultResult(String defaultResult) {
this.defaultResult = defaultResult;
}
public String getRequestId() {
return requestId;
}
public void setRequestId(String requestId) {
this.requestId = requestId;
}
public String getScalingActivityId() {
return scalingActivityId;
}
public void setScalingActivityId(String scalingActivityId) {
this.scalingActivityId = scalingActivityId;
}
public List<String> getInstanceIds() {
return instanceIds;
}
public void setInstanceIds(List<String> instanceIds) {
this.instanceIds = instanceIds;
}
}
MnsMessageModel.java
package model;
public class MnsMessageModel {
private String userId;
private String regionId;
private String resourceArn;
private HookModel content;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getRegionId() {
return regionId;
}
public void setRegionId(String regionId) {
this.regionId = regionId;
}
public String getResourceArn() {
return resourceArn;
}
public void setResourceArn(String resourceArn) {
this.resourceArn = resourceArn;
}
public HookModel getContent() {
return content;
}
public void setContent(HookModel content) {
this.content = content;
}
}
Maven 依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.aliyun.fc.wujin</groupId>
<artifactId>demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-ecs</artifactId>
<version>4.10.1</version>
</dependency>
<dependency>
<groupId>com.aliyun.fc.runtime</groupId>
<artifactId>fc-java-core</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>3.2.6</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-r-kvstore</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.25</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>com.springsource.org.apache.commons.lang</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>com.aliyun.mns</groupId>
<artifactId>aliyun-sdk-mns</artifactId>
<version>1.1.8.4</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<appendAssemblyId>false</appendAssemblyId> <!-- this is used for not append id to the jar name -->
</configuration>
<executions>
<execution>
<id>make-assembly</id> <!-- this is used for inheritance merges -->
<phase>package</phase> <!-- bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
上述java文件中,Example.java 文件在包名为 fc 的目录下,FCResult.java HookModel.java MnsMessageModel.java 三个文件在包名为 model 的目录下,package fc 与 package model 处于同级目录。
Example.java 文件需要根据实际情况对相关参数进行替换,QUEUE_NAME 参数定义了接收函数执行结果的 MNS 队列,我们在 配置 MNS 章节已经提前创建好了。
参数替换完成以后,可以参考 FC Java 编程说明 重新打包并上传您的 jar 包即可,上传方法如下图所示:
写在最后
通过 AutoScaling -> LifecycleHook -> MNS -> FC 的方式,您可以具备更加丰富的弹性能力,从而更加灵活地管理您伸缩组内的资源。
上述代码仅供参考,具体实现需要结合具体业务进行测试改造。