目前,彈性伸縮服務已經接入了負載均衡(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";@Overridepublic 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);//獲取本次伸縮活動對應實例的私網IPList<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 instancefor (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;}@Overridepublic 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 的方式,您可以具備更加豐富的彈性能力,從而更加靈活地管理您伸縮組內的資源。
上述代碼僅供參考,具體實現需要結合具體業務進行測試改造。