salesforce lightning零基礎學習(十五) 公用組件之 獲取表字段的Picklist(多語言),salesforce 零基礎學習(六十二)獲取sObject中類型為Picklist的field values(含record type)

此篇參考:

 我們在lightning中在前台會經常碰到獲取picklist的values然後使用select option進行渲染成下拉列表,此篇用於實現針對指定的sObject以及fieldName(Picklist類型)獲取此字段對應的所有可用的values的公用組件。因為不同的record type可能設置不同的picklist values,所以還有另外的參數設置針對指定的record type developer name去獲取指定的record type對應的Picklist values.

一. PicklistService公用組件聲明實現

Common_PicklistController.cls:有三個形參,其中objectName以及fieldName是必填參數,recordTypeDeveloperName為可選參數。

 1 public without sharing class Common_PicklistController {
 2     
 3     @AuraEnabled(cacheable=true)
 4     public static List<Map<String,String>> getPicklistValues(String objectName, String fieldName,String recordTypeDeveloperName) {
 5         //1. use object and field name get DescribeFieldResult and also get all the picklist values
 6         List<Schema.PicklistEntry> allPicklistValuesByField;
 7         try {
 8             List<Schema.DescribeSobjectResult> objDescriptions = Schema.describeSObjects(new List<String>{objectName});
 9             Schema.SObjectField field = objDescriptions[0].fields.getMap().get(fieldName);
10             Schema.DescribeFieldResult fieldDescribeResult = field.getDescribe();
11             allPicklistValuesByField = fieldDescribeResult.getPicklistValues();
12         } catch (Exception e) {
13             throw new AuraHandledException('Failed to retrieve values : '+ objectName +'.'+ fieldName +': '+ e.getMessage());
14         }
15         
16         //2. get all active field name -> label map
17         List<Map<String,String>> activeOptionMapList = new List<Map<String,String>>();
18         Map<String,String> optionValue2LabelMap = new Map<String,String>();
19         List<String> optionValueList;
20         for(Schema.PicklistEntry entry : allPicklistValuesByField) {
21             if (entry.isActive()) {
22                 System.debug(LoggingLevel.INFO, '*** entry: ' + JSON.serialize(entry));
23                 optionValue2LabelMap.put(entry.getValue(), entry.getLabel());
24             }
25         }
26 
27         //3. generate list with option value(with/without record type)
28         if(String.isNotBlank(recordTypeDeveloperName)) {
29             optionValueList = PicklistDescriber.describe(objectName,recordTypeDeveloperName,fieldName);
30         } else {
31             optionValueList = new List<String>(optionValue2LabelMap.keySet());
32         }
33 
34         //4. generate and format result
35         if(optionValueList != null) {
36             for(String option : optionValueList) {
37                 String optionLabel = optionValue2LabelMap.get(option);
38                 Map<String,String> optionDataMap = new Map<String,String>();
39                 optionDataMap.put('value',option);
40                 optionDataMap.put('label', optionLabel);
41                 activeOptionMapList.add(optionDataMap);
42             }
43         }
44 
45         return activeOptionMapList;
46     }
47 }

 Common_PicklistService.cmp:聲明了getPicklistInfo方法,有以下三個主要參數.objectName對應sObject的API名字,fieldName對應的此sObject中的Picklist類型的字段,recordTypeDeveloperName對應這個sObject的record type的developer name

1 <aura:component access="global" controller="Common_PicklistController">
2     <aura:method access="global" name="getPicklistInfo"  description="Retrieve active picklist values and labels mapping with(without) record type" action="{!c.getPicklistInfoAction}">
3         <aura:attribute type="String" name="objectName" required="true" description="Object name"/>
4         <aura:attribute type="String" name="fieldName" required="true" description="Field name"/>
5         <aura:attribute type="String" name="recordTypeDeveloperName" description="record type developer name"/>
6         <aura:attribute type="Function" name="callback" required="true" description="Callback function that returns the picklist values and labels mapping as [{value: String, label: String}]"/>
7     </aura:method>
8 </aura:component>

 Common_PicklistServiceController.js: 獲取傳遞過來的參數,調用後台方法並對結果放在callback中。

 1 ({
 2     getPicklistInfoAction : function(component, event, helper) {
 3         const params = event.getParam('arguments');
 4         const action = component.get('c.getPicklistValueList');
 5         action.setParams({
 6             objectName : params.objectName,
 7             fieldName : params.fieldName,
 8             recordTypeDeveloperName : params.recordTypeDeveloperName
 9         });
10         action.setCallback(this, function(response) {
11             const state = response.getState();
12             if (state === 'SUCCESS') {
13                 params.callback(response.getReturnValue());
14             } else if (state === 'ERROR') {
15                 console.error('failed to retrieve picklist values for '+ params.objectName +'.'+ params.fieldName);
16                 const errors = response.getError();
17                 if (errors) {
18                     console.error(JSON.stringify(errors));
19                 } else {
20                     console.error('Unknown error');
21                 }
22             }
23         });
24 
25         $A.enqueueAction(action);
26     }
27 })

二. 公用組件調用

 上面介紹了公用組件以後,下面的demo是如何調用。

SimplePicklistDemo引入Common_PicklistService,設置aura:id用於後期獲取到此component,從而調用方法

 1 <aura:component implements="flexipage:availableForAllPageTypes">
 2     <!-- include common picklist service component -->
 3     <c:Common_PicklistService aura:id="service"/>
 4     <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
 5 
 6     <aura:attribute name="accountTypeList" type="List"/>
 7     <aura:attribute name="accountTypeListByRecordType" type="List"/>
 8 
 9     <lightning:layout verticalAlign="center" class="x-large">
10         <lightning:layoutItem flexibility="auto" padding="around-small">
11             <lightning:select label="account type">
12                 <aura:iteration items="{!v.accountTypeList}" var="type">
13                     <option value="{!type.value}" text="{!type.label}"></option>
14                 </aura:iteration>
15             </lightning:select>
16         </lightning:layoutItem>
17 
18         <lightning:layoutItem flexibility="auto" padding="around-small">
19             <lightning:select label="account type with record type">
20                 <aura:iteration items="{!v.accountTypeListByRecordType}" var="type">
21                     <option value="{!type.value}" text="{!type.label}"></option>
22                 </aura:iteration>
23             </lightning:select>
24         </lightning:layoutItem>
25     </lightning:layout>
26     
27 </aura:component>

SimplePicklistDemoController.js:初始化方法用於獲取到公用組件component然後獲取Account的type的values,第一個是獲取所有的values/labels,第二個是獲取指定record type的values/labels。

 1 ({
 2     doInit : function(component, event, helper) {
 3         const service = component.find('service');
 4         service.getPicklistInfo('Account','type','',function(result) {
 5             component.set('v.accountTypeList', result);
 6         });
 7 
 8         service.getPicklistInfo('Account','type','Business_Account',function(result) {
 9             component.set('v.accountTypeListByRecordType',result);
10         });
11     }
12 })

三.效果展示:

1. account type的展示方式

 

 2. account type with record type的展示方式。

 

 總結:篇中介紹了Picklist values針對with/without record type的公用組件的使用,感興趣的可以進行優化,篇中有錯誤的歡迎指出,有不懂的歡迎留言。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

ASP.NET Aries 高級開發教程:如何寫WebAPI接口

前提:

最近,有不少同學又問到,Aries里如何提供WebAPI接口?

針對這個問題,今天給順路寫個教程,其實呢,很簡單的。

方式一:直接用WebService提供接口。

用這種方式,直接添加接口就可以了,Aries只是默認處理了.html後綴的請求。對於WS的asmx後綴是沒有影響的,所以傳統怎麼添加就怎麼添加。

 

方式二:單獨用Taurus.MVC寫一個接口項目。

用這種方式,就是把接口獨立成一個項目,然後通過IIS部署成子應用程序就可以了。

只是部署成子應用程序的時候,需要注意一下子目錄和根目錄的web.config,出現重複的只能留根目錄的那個。

(一般都會建議用戶用這種方式,好處是可以在IIS里學會一下怎麼部署子應用程序。)

方式三:在Aries引入Taurus.MVC即可。

這個方式,其實也很簡單,下面介紹一下簡單的部署:

1、引用Taurus.MVC用於寫WebAPI:

在Web.UI項目添加引用Taurus.Core.dll(可以在Nuget上引用,也可以引用源碼項目再引用項目)

2、配置Taurus.MVC的兩個必備項:

A、在HttpModule中添加URL攔截。

//這是原來有的: 
<add name="Aries.Core" type="Aries.Core.UrlRewrite,Aries.Core" />
//這是新添加的:
 <add name="Taurus.Core" type="Taurus.Core.UrlRewrite,Taurus.Core"/>

B、在AppSetting中設置路徑模式:

<!--配置模式【值為0,1或2】[默認為1]
      值為0:匹配{Action}/{Para}
      值為1:匹配{Controller}/{Action}/{Para}
      值為2:匹配{Module}/{Controller}/{Action}/{Para}-->
    <add key="Taurus.RouteMode" value="1"/>

C、在AppSetting中設置接口代碼所在的項目:

<!--指定控制器所在的項目(Dll)名稱(可改,項目編繹的dll叫什麼名就寫什麼)
    <add key="Taurus.Controllers" value="Taurus.Controllers"/>-->

 

如果是用Nuget上引用的,默認都會有上面的兩個,其它默認生的,可以註釋掉。

3、開始寫應用接口代碼:

接口代碼寫在哪裡呢?放在哪個項目都可以,只要上面C點的配置指向就可以了,如果接口代碼分散在多個項目中,配置的value可以用“逗號”分隔。

按Taurus.MVC的方式寫接口,繼承自Taurus.Core.Controller即可:

如:

  /// <summary>
    /// API 接口
    /// </summary>
    public  class APIController : Taurus.Core.Controller
    {
        public void Hello()
        {
            Write("hello Controllers.API");
        }
    }

接口訪問:http://…/api/hello

總結說明:

Aries中默認處理的是.html後綴。

Taurus默認處理的是無後綴。

所以兩者並無衝突,直接引用,加配置就可以了,沒你想的複雜。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

讀寫分離很難嗎?springboot結合aop簡單就實現了

目錄

前言

入職新公司到現在也有一個月了,完成了手頭的工作,前幾天終於有時間研究下公司舊項目的代碼。在研究代碼的過程中,發現項目里用到了Spring Aop來實現數據庫的讀寫分離,本着自己愛學習(我自己都不信…)的性格,決定寫個實例工程來實現spring aop讀寫分離的效果。

環境部署

數據庫:MySql

庫數量:2個,一主一從

關於mysql的主從環境部署之前已經寫過文章介紹過了,這裏就不再贅述,參考

開始項目

首先,毫無疑問,先開始搭建一個SpringBoot工程,然後在pom文件中引入如下依賴:

<dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>2.1.5</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <!-- 動態數據源 所需依賴 ### start-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- 動態數據源 所需依賴 ### end-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
    </dependencies>

目錄結構

引入基本的依賴后,整理一下目錄結構,完成后的項目骨架大致如下:

建表

創建一張表user,在主庫執行sql語句同時在從庫生成對應的表數據

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `user_id` bigint(20) NOT NULL COMMENT '用戶id',
  `user_name` varchar(255) DEFAULT '' COMMENT '用戶名稱',
  `user_phone` varchar(50) DEFAULT '' COMMENT '用戶手機',
  `address` varchar(255) DEFAULT '' COMMENT '住址',
  `weight` int(3) NOT NULL DEFAULT '1' COMMENT '權重,大者優先',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `user` VALUES ('1196978513958141952', '測試1', '18826334748', '廣州市海珠區', '1', '2019-11-20 10:28:51', '2019-11-22 14:28:26');
INSERT INTO `user` VALUES ('1196978513958141953', '測試2', '18826274230', '廣州市天河區', '2', '2019-11-20 10:29:37', '2019-11-22 14:28:14');
INSERT INTO `user` VALUES ('1196978513958141954', '測試3', '18826273900', '廣州市天河區', '1', '2019-11-20 10:30:19', '2019-11-22 14:28:30');

主從數據源配置

application.yml,主要信息是主從庫的數據源配置

server:
  port: 8001
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    master:
      url: jdbc:mysql://127.0.0.1:3307/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&useSSL=false&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
      username: root
      password:
    slave:
      url: jdbc:mysql://127.0.0.1:3308/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&useSSL=false&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
      username: root
      password:

因為有一主一從兩個數據源,我們用枚舉類來代替,方便我們使用時能對應

@Getter
public enum DynamicDataSourceEnum {
    MASTER("master"),
    SLAVE("slave");
    private String dataSourceName;
    DynamicDataSourceEnum(String dataSourceName) {
        this.dataSourceName = dataSourceName;
    }
}

數據源配置信息類 DataSourceConfig,這裏配置了兩個數據源,masterDb和slaveDb

@Configuration
@MapperScan(basePackages = "com.xjt.proxy.mapper", sqlSessionTemplateRef = "sqlTemplate")
public class DataSourceConfig {
    
     // 主庫
      @Bean
      @ConfigurationProperties(prefix = "spring.datasource.master")
      public DataSource masterDb() {
  return DruidDataSourceBuilder.create().build();
      }

    /**
     * 從庫
     */
    @Bean
    @ConditionalOnProperty(prefix = "spring.datasource", name = "slave", matchIfMissing = true)
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDb() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 主從動態配置
     */
    @Bean
    public DynamicDataSource dynamicDb(@Qualifier("masterDb") DataSource masterDataSource,
        @Autowired(required = false) @Qualifier("slaveDb") DataSource slaveDataSource) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DynamicDataSourceEnum.MASTER.getDataSourceName(), masterDataSource);
        if (slaveDataSource != null) {
            targetDataSources.put(DynamicDataSourceEnum.SLAVE.getDataSourceName(), slaveDataSource);
        }
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        return dynamicDataSource;
    }
    @Bean
    public SqlSessionFactory sessionFactory(@Qualifier("dynamicDb") DataSource dynamicDataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setMapperLocations(
            new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*Mapper.xml"));
        bean.setDataSource(dynamicDataSource);
        return bean.getObject();
    }
    @Bean
    public SqlSessionTemplate sqlTemplate(@Qualifier("sessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
    @Bean(name = "dataSourceTx")
    public DataSourceTransactionManager dataSourceTx(@Qualifier("dynamicDb") DataSource dynamicDataSource) {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dynamicDataSource);
        return dataSourceTransactionManager;
    }
}

設置路由

設置路由的目的為了方便查找對應的數據源,我們可以用ThreadLocal保存數據源的信息到每個線程中,方便我們需要時獲取

public class DataSourceContextHolder {
    private static final ThreadLocal<String> DYNAMIC_DATASOURCE_CONTEXT = new ThreadLocal<>();
    public static void set(String datasourceType) {
        DYNAMIC_DATASOURCE_CONTEXT.set(datasourceType);
    }
    public static String get() {
        return DYNAMIC_DATASOURCE_CONTEXT.get();
    }
    public static void clear() {
        DYNAMIC_DATASOURCE_CONTEXT.remove();
    }
}

獲取路由

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.get();
    }
}

AbstractRoutingDataSource的作用是基於查找key路由到對應的數據源,它內部維護了一組目標數據源,並且做了路由key與目標數據源之間的映射,提供基於key查找數據源的方法。

數據源的註解

為了可以方便切換數據源,我們可以寫一個註解,註解中包含數據源對應的枚舉值,默認是主庫,

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSourceSelector {

    DynamicDataSourceEnum value() default DynamicDataSourceEnum.MASTER;
    boolean clear() default true;
}

aop切換數據源

到這裏,aop終於可以現身出場了,這裏我們定義一個aop類,對有註解的方法做切換數據源的操作,具體代碼如下:

@Slf4j
@Aspect
@Order(value = 1)
@Component
public class DataSourceContextAop {

 @Around("@annotation(com.xjt.proxy.dynamicdatasource.DataSourceSelector)")
    public Object setDynamicDataSource(ProceedingJoinPoint pjp) throws Throwable {
        boolean clear = true;
        try {
            Method method = this.getMethod(pjp);
            DataSourceSelector dataSourceImport = method.getAnnotation(DataSourceSelector.class);
            clear = dataSourceImport.clear();
            DataSourceContextHolder.set(dataSourceImport.value().getDataSourceName());
            log.info("========數據源切換至:{}", dataSourceImport.value().getDataSourceName());
            return pjp.proceed();
        } finally {
            if (clear) {
                DataSourceContextHolder.clear();
            }

        }
    }
    private Method getMethod(JoinPoint pjp) {
        MethodSignature signature = (MethodSignature)pjp.getSignature();
        return signature.getMethod();
    }

}

到這一步,我們的準備配置工作就完成了,下面開始測試效果。

先寫好Service文件,包含讀取和更新兩個方法,

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @DataSourceSelector(value = DynamicDataSourceEnum.SLAVE)
    public List<User> listUser() {
        List<User> users = userMapper.selectAll();
        return users;
    }

    @DataSourceSelector(value = DynamicDataSourceEnum.MASTER)
    public int update() {
        User user = new User();
        user.setUserId(Long.parseLong("1196978513958141952"));
        user.setUserName("修改后的名字2");
        return userMapper.updateByPrimaryKeySelective(user);
    }

    @DataSourceSelector(value = DynamicDataSourceEnum.SLAVE)
    public User find() {
        User user = new User();
        user.setUserId(Long.parseLong("1196978513958141952"));
        return userMapper.selectByPrimaryKey(user);
    }
}

根據方法上的註解可以看出,讀的方法走從庫,更新的方法走主庫,更新的對象是userId為1196978513958141953 的數據,

然後我們寫個測試類測試下是否能達到效果,

@RunWith(SpringRunner.class)
@SpringBootTest
class UserServiceTest {

    @Autowired
    UserService userService;

    @Test
    void listUser() {
        List<User> users = userService.listUser();
        for (User user : users) {
            System.out.println(user.getUserId());
            System.out.println(user.getUserName());
            System.out.println(user.getUserPhone());
        }
    }
    @Test
    void update() {
        userService.update();
        User user = userService.find();
        System.out.println(user.getUserName());
    }
}

測試結果:

1、讀取方法

2、更新方法

執行之後,比對數據庫就可以發現主從庫都修改了數據,說明我們的讀寫分離是成功的。當然,更新方法可以指向從庫,這樣一來就只會修改到從庫的數據,而不會涉及到主庫。

注意

上面測試的例子雖然比較簡單,但也符合常規的讀寫分離配置。值得說明的是,讀寫分離的作用是為了緩解寫庫,也就是主庫的壓力,但一定要基於數據一致性的原則,就是保證主從庫之間的數據一定要一致。如果一個方法涉及到寫的邏輯,那麼該方法里所有的數據庫操作都要走主庫

假設寫的操作執行完后數據有可能還沒同步到從庫,然後讀的操作也開始執行了,如果這個讀取的程序走的依然是從庫的話,那麼就會出現數據不一致的現象了,這是我們不允許的。

最後發一下項目的github地址,有興趣的同學可以看下,記得給個star哦

地址:

參考:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

USB CONNECTOR 掌控什麼技術要點? 帶您認識其相關發展及效能

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

4 個概念,1 個動作,讓應用管理變得更簡單

作者:
劉洋(炎尋) EDAS-OAM 架構與開發負責人
鄧洪超  OAM spec maintainer
孫健波(天元)  OAM spec maintainer

隨着以 K8s 為主的雲原生基礎架構遍地生根,越來越多的團隊開始基於 K8s 搭建持續部署、自助式發布體驗的應用管理平台。然而,在 K8s 交付和管理應用方面,目前還缺乏一個統一的標準,這最終促使我們與微軟聯合推出了首個雲原生應用標準定義與架構模型 – OAM。本文作者將從基本概念以及各個模塊的封裝設計與用法等角度出發來詳細解讀 OAM。

OAM 主要有三個特點:

  • 開發和運維關注點分離:開發者關注業務邏輯,運維人員關注運維能力,讓不同角色更專註於領域知識和能力;
  • 平台無關與高可擴展:應用定義與平台實現解耦,應用描述支持跨平台實現和可擴展性;
  • 模塊化應用部署和運維特徵:應用部署和運維能力可以描述成高層抽象模塊,開發和運維可以自由組合和支持模塊化實現。

OAM 綜合考慮了在公有雲、私有雲以及邊緣雲上應用交付的解決方案,提出了通用的模型,讓各平台可以在統一的高層抽象上透出應用部署和運維能力,解決跨平台的應用交付問題。同時,OAM 以標準化的方式溝通和連接應用開發者、運維人員、應用基礎設施,讓雲原生應用交付和管理流程更加連貫、一致。

角色分類

OAM 將應用相關的人員劃分為 3 個角色:

  • 應用開發:關注應用代碼開發和運行配置,是應用代碼的領域專家,應用開發完成后打包(比如鏡像)交給應用運維;

  • 應用運維:關注配置和運行應用實例的生命周期,比如灰度發布、監控、報警等操作,是應用運維專家;

  • 平台運維:關注應用運行平台的能力和穩定性,是底層(比如 Kubernetes 運維/優化,OS 等)的領域專家。

核心概念

OAM 包含以下核心概念:

服務組件(Component Schematics)

應用開發使用服務組件來聲明應用的屬性(配置項),運維人員定義這些屬性之後就能按照組件聲明得到運行的組件實例,組件聲明包含以下信息:

  • 工作負載類型(Workload type):表明該組件運行時的工作負載依賴;
  • 元數據(Metadata):面向組件用戶的一些描述性信息;
  • 資源需求(Resource requirements):組件運行的最小資源需求,比如最小內存,CPU 和文件掛載需求;
  • 參數(Parameters):可以被運維人員配置的參數;
  • 工作負載定義(Workload definition):工作負載運行的一些定義,比如可運行包定義(ICO images, Function等)。

應用邊界(Application Scopes)

運維人員使用應用邊界將組件組成松耦合的應用,可以賦予這組組件一些共用的屬性和依賴,應用邊界聲明包含以下信息:

  • 元數據(Metadata):面嚮應用邊界用戶的一些描述性信息。
  • 類型(Type):邊界類型,不同類型提供不同的能力;
  • 參數(Parameters):可以被運維人員配置的參數。

運維特徵(Traits)

運維人員使用運維特徵賦予組件實例特定的運維能力,比如自動擴縮容,一個 Trait 可能僅限特定的工作負載類型,它們代表了系統運維方面的特性,而不是開發的特性,比如開發者知道自己的組件是否可以擴縮容,但是運維可以決定是手動擴縮容還是自動擴縮容,特徵聲明包含以下信息:

  • 元數據(Metadata):面向特徵用戶的一些描述性信息;
  • 適用工作負載列表(Applies-to list):該特徵可以應用的工作負載列表;
  • 屬性(Properties):可以被運維人員配置的屬性。

工作負載類型和配置(Workload types and configurations)

描述特定工作負載的底層運行時,平台需要能夠提供對應工作負載的運行時,工作負載聲明包含以下信息:

  • 元數據(Metadata):面向工作負載用戶的一些描述性信息;
  • 工作負載設置(Workload Setting):可以被運維人員配置的設置。

應用配置(Application configuration)

運維人員使用應用配置將組件、特徵和應用邊界的組合在一起實例化部署,應用配置聲明包含以下信息:

  • 元數據(Metadata):面嚮應用配置用戶的一些描述性信息;
  • 參數覆蓋(Parameter overrides):可以理解為變量定義,可以被組件、特徵、應用邊界的參數引用;
  • 組件設置(Component):構成應用的全部組件都在這裏設置;
  • 綁定組件的運維特徵配置(Trait Configuration):綁定的特徵列表及其參數。

OAM 認為:

一個雲原生應用由一組相互關聯但又離散獨立的組件構成,這些組件實例化在合適的運行時上,由配置來控制行為並共同協作提供統一的功能。

更加具體的說:

一個 Application 由一組 Components 構成,每個 Component 的運行時由 Workload 描述,每個 Component 可以施加 Traits 來獲取額外的運維能力,同時我們可以使用 Application scopes 將 Components 劃分到 1 或者多個應用邊界中,便於統一做配置、限制、管理。

整體的運行模式如下所示:

組件、運維特徵、應用邊界通過應用配置(Application Configuration)實例化,然後再通過 OAM 的實現層翻譯為真實的資源。

怎麼用?

使用 OAM 來管理雲原生應用,其核心主要是圍繞着“四個概念,一個動作”。

四個概念

  • 應用組件(包含對工作負載的依賴聲明);【開發人員關心】
  • 工作負載;【平台關心】
  • 運維特徵;【平台關心】
  • 應用邊界;【平台關心】

一個動作

  • 下發應用配置;【運維人員關心】

下發應用配置之後 OAM 平台將會實例化四個概念得到運行的應用。
 
一個 OAM 平台在對外提供 OAM 應用管理界面時,一定是預先已經準備好了“運維特徵,應用邊界”供運維人員使用,準備好了“工作負載”供開發者使用,同時開發者準備“組件”供運維人員使用。因此角色劃分就很明了:

  • 開發者提供組件,使用平台的工作負載;
  • 運維人員關注一個動作實例化四個概念;
  • 平台方需要提供工作負載,運維特徵,應用邊界,並且託管用戶的應用組件;

例子

如何使用上面“四個概念,一個動作”來部署一個 OAM 應用呢?

我們假想一個場景,用戶的應用有兩個組件:frontend 和 backend,其中 frontend 組件需要域名訪問、自動擴縮容能力,並且 frontend 要訪問 backend,兩者應該在同一個 vpc 內,基於這個場景我們需要有:

  • 開發者創建 2 個組件

frontend 的 workload 類型是 Server 容器服務(OAM 平台提供該工作負載):

apiVersion: core.oam.dev/v1alpha1
kind: ComponentSchematic
metadata:
  name: frontend
  annotations:
    version: v1.0.0
    description: "A simple webserver"
spec:
  workloadType: core.oam.dev/v1.Server
  parameters:
    - name: message
      description: The message to display in the web app.
      type: string
      value: "Hello from my app, too"
  containers:
    - name: web
      env:
        - name: MESSAGE
          fromParam: message
      image:
        name: example/charybdis-single:latest

backend 的 workload 是 Cassandra(OAM 平台提供該工作負載):

apiVersion: core.oam.dev/v1alpha1
kind: ComponentSchematic
metadata:
  name: backend
  annotations:
    version: v1.0.0
    description: "Cassandra database"
spec:
  workloadType: data.oam.dev/v1.Cassandra
  parameters:
    - name: maxStalenessPrefix
      description: Max stale requests.
      type: int
      value: 100000
    - name: defaultConsistencyLevel
      description: The default consistency level
      type: string
      value: "Eventual"
  workloadSettings:
    - name: maxStalenessPrefix
      fromParam: maxStalenessPrefix
    - name: defaultConsistencyLevel
      fromParam: defaultConsistencyLevel

 

  • OAM 平台提供 Ingress Trait
apiVersion: core.oam.dev/v1alpha1
kind: Trait
metadata:
  name: Ingress
spec:
  type: core.oam.dev/v1beta1.Ingress
  appliesTo:
    - core.oam.dev/v1alpha1.Server
  parameters:
    properties: |
      {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "host": {
            "type": "string",
            "description": "ingress hosts",
          },
          "path": {
            "type": "string",
            "description": "ingress path",
          }
        }
      }

 

  • OAM 平台提供網絡應用邊界
apiVersion: core.oam.dev/v1alpha1
kind: ApplicationScope
metadata:
  name: network
  annotations:
    version: v1.0.0
    description: "network boundary that a group components reside in"
spec:
  type: core.oam.dev/v1.NetworkScope
  allowComponentOverlap: false
  parameters:
    - name: network-id
      description: The id of the network, e.g. vpc-id, VNet name.
      type: string
      required: Y
    - name: subnet-ids
      description: >
        A comma separated list of IDs of the subnets within the network. For example, "vsw-123" or ""vsw-123,vsw-456".
        There could be more than one subnet because there is a limit in the number of IPs in a subnet.
        If IPs are taken up, operators need to add another subnet into this network.
      type: string
      required: Y
    - name: internet-gateway-type
      description: The type of the gateway, options are 'public', 'nat'. Empty string means no gateway.
      type: string
      required: N

 

apiVersion: core.oam.dev/v1alpha1
kind: ApplicationConfiguration
metadata:
  name: my-vpc-network
spec:
  variables:
    - name: networkName
      value: "my-vpc"
  scopes:
    - name: network
      type: core.oam.dev/v1alpha1.Network
      properties:
        - name: network-id
          value: "[fromVariable(networkName)]"
        - name: subnet-ids
          value: "my-subnet1, my-subnet2"

 

  • 應用運維部署整個應用

使用應用配置將上面的組件、邊界、特徵組合起來即可部署一個應用,這個由應用的運維人員提供:

apiVersion: core.oam.dev/v1alpha1
kind: ApplicationConfiguration
metadata:
  name: custom-single-app
  annotations:
    version: v1.0.0
    description: "Customized version of single-app"
spec:
  variables:
    - name: message
      value: "Well hello there"
    - name: domainName
      value: "www.example.com"
  components:
    - componentName: frontend
      instanceName: web-front-end
      parameterValues:
        - name: message
          value: "[fromVariable(message)]"
      traits:
        - name: Ingress
          properties:
            - name: host
              value: "[fromVaraible(domainName)]"
            - name: path
              value: "/"
      applicationScopes:
        - my-vpc-network

    - componentName: backend
      instanceName: database
      applicationScopes:
        - my-vpc-network

通過以上完整的使用流程我們可以看到:應用配置是真正部署應用的起點,在使用該配置的時候,對應的組件、應用邊界、特徵都要提前部署好,運維人員只需做一個組合即可。

服務組件(Component Schematic)

服務組件的意義是讓開發者聲明離散執行單元的運行時特性,可以用 JSON/YAML 格式來表示,其聲明遵循 Kubernetes API 規範,區別在於:

  • 定義字段是 Kubernetes 的子集,因為 OAM 是更高的抽象;
  • OAM Spec 的底層運行時可以不是 Kubernetes

下面我們來看看具體的組件聲明如何定義。

定義

頂層屬性

屬性 類型 必填 默認值 描述
apiVersion string Y 特定oam spec版本,比如core.oam.dev/v1
kind string Y 類型,對於組件來說就是
ComponentSchematic
metadata Metadata Y 組件元數據
spec Spec Y 組件特定的定義屬性

Metadata

屬性 類型 必填 默認值 描述
name string Y
labels map[string]string N k/v對作為組件的lebels
annotations map[string]string N k/v對作為組件的描述信息

Spec

屬性 類型 必填 默認值 描述
parameters []Parameter N 組件的可配置項
workloadType string Y 簡明語義化的組件運行時描述,以K8s為例就是指定底層使用StatefulSet還是Deployment這樣
osType string N linux 組件容器運行時依賴的操作系統類型,可選值:
– linux
– windows
arch string N amd64 組件容器運行時依賴的CPU架構,可選值:
– i386
– amd64
– arm
– arm64
containers []Container N 實現組件的OCI容器們
workloadSettings []WorkloadSettings N 需要傳給工作負載運行時的非容器配置聲明

上面的 workloadType 後面會有詳細說明,這裏簡要來說就是開發者可以指定該 field 告訴運行時該組件如何被執行。工作負載命名的規範是 GROUP/VERSION.KIND,和 K8s 資源類型坐標一致,比如:

  • core.oam.dev/v1alpha1.Singleton

core.oam.dev 表示是 oam 內置的分組,oam 內置的表示任何 OAM 實現都支持該類型的工作負載,v1alpha1 表示依舊是 alpha 狀態,類型是 Singleton,這表示運行時應該只運行一個組件實例,無論誰提供。

  • alibabacloud.com/v1.Function

alibabacloud.com 表示該對象是一個運營商特定的實現,不一定所有平台都實現,版本 v1 表示實現已經穩定,類型是 Function 表示運行時是 Alibaba Functions 提供的。

  • streams.oam.io/v1beta2.Kafka

表示該對象是一個第三方組織實現,不一定所有平台都實現,版本 v1beta2 表示實現已經趨於穩定,類型是 Kafka 表示運行時是開源組件 Kafka 提供的。

工作負載主要分為兩類:

  • 核心工作負載類型

屬於 core.oam.dev 分組,所有對象都需要 oam 平台實現,都是容器運行時,該 Spec 目前定義了如下核心工作負載類型:

名字 類型 是否對外提供服務 是否可以多副本運行 是否常駐
Server core.oam.dev/v1alpha1.Server Y Y Y
Singleton Server core.oam.dev/v1alpha1.SingletonServer Y N Y
Worker core.oam.dev/v1alpha1.Worker N Y Y
Singleton
Worker
core.oam.dev/v1alpha1.SingletonWorker N N Y
Task core.oam.dev/v1alpha1.Task N Y N
Singleton Task core.oam.dev/v1alpha1.SingletonTask N N N
  • Server:定義了容器運行時可以運行 0 或多個容器實例,該工作負載提供了冗餘的可擴縮容的多副本常駐服務,在 K8s 平台可以使用 Deployment 或者 Statefulset 加上 Service 來實現;

  • Singleton Server:定義了容器運行時只能運行一個容器實例,該工作負載提供了無法冗餘的單副本常駐服務,在 K8s 平台可以使用副本為 1 的 Statefulset 加上 Service 來實現;

  • Worker:定義了容器運行時可以運行 0 或多個容器實例,該工作負載提供了冗餘的可擴縮容的多副本常駐實例但是不提供服務,在 K8s 平台可以使用 Deployment 或者 Statefulset 來實現;

  • Singleton Worker:定義了容器運行時只能運行一個容器實例,該工作負載提供了無法冗餘的單副本常駐實例但是不提供服務,在 K8s 平台可以使用副本為 1 的 Statefulset 來實現;

  • Task:定義了非常駐可冗餘的容器運行時,運行完就退出,可以賦予擴縮容特性,在 K8s 平台可以使用 Job 來實現;

  • Singleton Task:定義了非常駐非冗餘的容器運行時,運行完就退出,在 K8s 平台可以使用 completions=1 的 Job 來實現。

核心工作負載必須給定 container 部分,實現核心工作負載的 OAM 平台必須不依賴 workloadSettings。

  • 擴展工作負載類型

擴展工作負載類型是平台運行時特定的,由各個 OAM 平台自定提供,當前版本的 Spec 不支持用戶自定義工作負載類型,只能是平台方提供,擴展工作負載可以使用非容器運行時,workloadSettings 的目的就是為了擴展工作負載類型的配置。

工作負載的定義是包羅萬象的,他允許任何部署的服務作為工作負載,無論是容器化還是虛擬機,基於此,我們可以將緩存,數據庫,消息隊列都作為工作負載,如果組件指定的工作負載在平台沒有提供,應該快速失敗將信息返回給用戶。

除了 WorkloadType,可以看到組件 Spec 內嵌了 3 種結構體,下面來看看它們的定義:

1.Parameter

定義了該組件的所有可配置項,其定義如下:

屬性 類型 必填 默認值 描述
name string Y
description string N 組件的簡短描述
type string Y 參數類型,JSON定義的boolean, number, … 
required boolean N false 參數值是否必須提供
default 同上面的type N 參數的默認值

2.Container

屬性 類型 必填 默認值 描述
name string Y 容器名字,在組件內必須唯一
iamge string Y 鏡像地址
resources Resources Y 鏡像運行最小資源需求
env []Env N 環境變量
ports []Port N 暴露端口
livenessProde HealthProbe N 健康狀態檢查指令
readinessProbe HealthProbe N 流量可服務狀態檢查指令
cmd []string N 容器運行入口
args []string N 容器運行參數
config []ConfigFile N 容器內可以訪問的配置文件
imagePullSecrets string N 拉取容器的憑證

其中 Resources 的定義如下:

屬性 類型 必填 默認值 描述
cpu CPU Y 容器運行所需的cpu資源
memory Memory Y 容器運行所需的memory資源
gpu GPU N 容器運行所需的gpu資源
volumes []Volume N 容器運行所需的存儲資源
extended []ExtendedResource N 容器運行所需的外部資源

其中 CPU/Memory/GPU 都是數值型,不贅述,Volume 的定義如下:

屬性 類型 必填 默認值 描述
name string Y 數據卷的名字,引用的時候使用
mountPath string Y 實際在文件系統的掛載路徑
accessMode string N RW 訪問模式,RW或RO
sharingPolicy string N Exclusive 掛載共享策略,Exclusive或者Shared
disk Disk N 該數據卷使用底層磁盤資源的屬性

Disk 指定存儲是否需要持久化,最小的空間需求,定義如下:

屬性 類型 必填 默認值 描述
required string Y 最小磁盤大小需求
ephemeral boolean N 是否需要掛載外部磁盤

ExtendedResource 描述特定實現的資源需求,比如 OAM 運行時平台可能提供特殊的硬件,這個字段允許容器聲明需要這個特定的 offering:

屬性 類型 必填 默認值 描述
required string Y 需要的條件
name string Y 資源名字,比如GV.K

Env,Port,HealthProbe 和 K8s 類似,這裏不再贅述。

3.WorkloadSetting

工作負載的附加配置,用於非容器運行時(當然,也可以用於容器運行時工作負載的附加字段)。

屬性 類型 必填 默認值 描述
name string Y 參數名
type string N string 參數類型,用於實現的hint
value any N 參數值,如果沒有fromParam則用該值
fromParam string N 參數引用,覆蓋value

這組配置會傳給運行時,一個運行時可以返回錯誤,如果特定的限制沒有滿足,比如:

  • 丟失了期望的 k/v 對;
  • 不認識的 k/v 對;

例子

使用核心工作負載 Server 的組件聲明

apiVersion: core.oam.dev/v1alpha1
kind: ComponentSchematic
metadata:
  name: frontend
  annotations:
    version: v1.0.0
    description: >
      Sample component schematic that describes the administrative interface for our Twitter bot.
spec:
  workloadType: core.oam.dev/v1alpha1.Server
  osType: linux
  parameters:
  - name: username
    description: Basic auth username for accessing the administrative interface
    type: string
    required: true
  - name: password
    description: Basic auth password for accessing the administrative interface
    type: string
    required: true
  - name: backend-address
    description: Host name or IP of the backend
    type: string
    required: true
  containers:
  - name: my-twitter-bot-frontend
    image:
      name: example/my-twitter-bot-frontend:1.0.0
      digest: sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b
    resources:
      cpu:
        required: 1.0
      memory:
        required: 100MB
    ports:
    - name: http
      value: 8080
    env:
    - name: USERNAME
      fromParam: 'username'
    - name: PASSWORD
      fromParam: 'password'
    - name: BACKEND_ADDRESS
      fromParam: 'backend-address'
    livenessProbe:
      httpGet:
        port: 8080
        path: /healthz
    readinessProbe:
      httpGet:
        port: 8080
        path: /healthz

使用擴展工作負載的組件聲明

apiVersion: core.oam.dev/v1alpha1
kind: ComponentSchematic
metadata:
  name: alibabacloudFunctions
  annotations:
    version: v1.0.0
    description: "Extended workflow example"
spec:
  workloadType: alibabacloud.com/v1.Function
  parameters:
  - name: github-token
    description: GitHub API session key
    type: string
    required: true
  workloadSettings:
    - name: source
      value: git://git.example.com/function/myfunction.git
    - name: github_token
      fromParam: github-token

總結

組件聲明是由開發者或者 OAM 平台給出,透出應用運行的可配置項、依賴的平台和工作負載,可以看成是一個聲明了運行環境的函數定義,運維人員填寫函數參數之後,組件就會按照聲明的功能運行起來。

應用邊界(Application scopes)

應用邊界通過提供不同形式的應用邊界以及共有的分組行為來將組件組成邏輯的應用,應用邊界具備以下通用的特徵:

  • 應用邊界應該描述該組組件實例的共有行為和元數據;
  • 一個組件可以同時部署到多個不同類型的應用邊界中;
  • 應用邊界類型可以決定組件是否可以部署到多個相同的應用邊界類型實例;
  • 應用邊界可以用於不同組件分組以及不同 infra 能力之間的連接機制,比如 networking 或者外部能力(如驗證服務);

下圖說明了組件可以屬於多個重疊的應用分組,最終創建出不同的應用邊界。

上圖有兩種應用邊界類型:Network 與 Health,有四個組件分佈在不同的應用邊界實例中:

  • A、B、C 三個組件部署到了相同的 Health scope,該 scope 會收集所有屬於這個邊界的組件狀態和信息,可以給 Traits 或者其他組件使用;
  • 組件 A 和 B、C、D 的網絡邊界是隔離的,這允許 infra 運維提供不同的 SDN 設置,控制不同分組的流量流入/流出規則;

類型

主要是有兩種應用邊界類型:

  • 核心應用邊界類型
  • 擴展應用邊界類型

核心應用邊界類型

定義基本運行時行為的分組結構,它們擁有如下特徵:

  • 必須是 core.oam.dev 命名空間;
  • 必須被全部實現;
  • 核心工作負載類型實例必須部署到所有核心應用邊界類型實例中;
  • 運行時必須為每種應用邊界類型提供默認的應用邊界實例;
  • 運行時如果組件實例沒有指定特定的應用邊界,必須將該組件實例部署到默認應用邊界實例上;

當前 Spec 定義的應用邊界類型有:

Name Type Description
Network core.oam.dev/v1alpha1.Network 該邊界將組件組織到一個子網邊界並且定義統一的運行時網絡模型,以及infra網絡描述的定義和規則
Health core.oam.dev/v1alpha1.Health 該邊界將組件組織到一個聚合的健康組中,信息可以用於回滾和升級

擴展應用邊界類型

對於運行時來說是 optional 的,可以自定義。

定義

apiVersion,kind,metadata 和前面組件一致,不贅述,主要描述 Spec:

屬性 類型 必填 默認值 描述
type string Y 應用邊界類型
allowComponentOverlap bool Y 決定是否允許一個組件同時出現在多個該類型應用邊界實例中
parameters []Parameter N 邊界的可配置參數

例子

Network scope(core)

用於將組件劃分到一個網絡或者 SDN 中,網絡本身必須被 infra 定義和運維,也可以被流量管理 Traits 查詢,用於發現 service mesh 的可發現邊界或者 API 網關的 API 邊界:

apiVersion: core.oam.dev/v1alpha1
kind: ApplicationScope
metadata:
  name: network
  annotations:
    version: v1.0.0
    description: "network boundary that a group components reside in"
spec:
  type: core.oam.dev/v1.NetworkScope
  allowComponentOverlap: false
  parameters:
    - name: network-id
      description: The id of the network, e.g. vpc-id, VNet name.
      type: string
      required: Y
    - name: subnet-id
      description: The id of the subnet within the network.
      type: string
      required: Y
    - name: internet-gateway-type
      description: The type of the gateway, options are 'public', 'nat'. Empty string means no gateway.
      type: string
      required: N

Health scope(core)

用於聚合組件的健康狀態,可以設計的參數有健康閾值(超過該閾值的組件不健康則認為整個邊界不健康),健康邊界實例本身不會用健康狀態做任何操作,它只是分組健康聚合器,其信息可以被查詢並用於其他地方,比如:

  • 應用的變更 Traits 可以監控健康狀態來決定何時回滾;
  • 監控 Traits 可以監控健康狀態來觸發報警。
apiVersion: core.oam.dev/v1alpha1
kind: ApplicationScope
metadata:
  name: health
  annotations:
    version: v1.0.0
    description: "aggregated health state for a group of components."
spec:
  type: core.oam.dev/v1alpha1.HealthScope
  allowComponentOverlap: true
  parameters:
    - name: probe-method
      description: The method to probe the components, e.g. 'httpGet'.
      type: string
      required: true
    - name: probe-endpoint
      description: The endpoint to probe from the components, e.g. '/v1/health'.
      type: string
      required: true
    - name: probe-timeout
      description: The amount of time in seconds to wait when receiving a response before marked failure.
      type: integer
      required: false
    - name: probe-interval
      description: The amount of time in seconds between probing tries.
      type: integer
      required: false
    - name: failure-rate-threshold
      description: If the rate of failure of total probe results is above this threshold, declared 'failed'.
      type: double
      required: false
    - name: healthy-rate-threshold
      description: If the rate of healthy of total probe results is above this threshold, declared 'healthy'.
      type: double
      required: false
    - name: health-threshold-percentage
      description: The % of healthy components required to upgrade scope
      type: double
      required: false
    - name: required-healthy-components
      description: Comma-separated list of names of the components required to be healthy for the scope to be health.
      type: []string
      required: false

resource quota scope(extended)

限制分組內所有組件的資源使用總量上限。

apiVersion: core.oam.dev/v1alpha1
kind: ApplicationScope
metadata:
  name: myResourceQuotas
  annotations:
    version: v1.0.0
    description: "The production configuration for Corp CMS"
spec:
  type: resources.oam.dev/v1.ResourceQuotaScope
  allowComponentOverlap: false
  parameters:
    - name: CPU
      description: maximum CPU to be consumed by this scope
      type: double
      required: Y
    - name: Memory
      description: maximum memory to be consumed by this scope
      type: double
      required: Y

總結

應用邊界聲明由 OAM 平台提供,透出應用邊界實例運行的可配置項,可以看成是一個函數定義,運維人員或者平台填寫函數參數之後,應用邊界就會按照聲明的功能運行起來,對該邊界內的組件們起作用。

應用特徵(Traits)

OAM Spec 的實現平台應該提供 Traits 給組件工作負載增強運維操作,一個 Trait 是一種自由的運行時,增強工作負載提供額外的功能,比如流量路由規則、自動擴縮容規則、升級策略等,這讓應用運維具備根據需求配置組件,不需要開發者參与的能力。一個獨立的 Trait 可以綁定 1 或多個工作負載類型,它可以聲明哪些工作負載類型才能使用該Trait。

規則

  • 目前並沒有機制來显示約定組件的多個 Traits 組合,也就是一個組件應用了 Trait A 無法要求 Trait B 必須應用於該組件,如果在運行時發生存在 Trait A 但是 Trait B 不存在,應該標記 Trait A 失敗;
  • Traits 應該按照定義的順序施加到組件上;
  • 應用部署只有當所有組件和其 Traits 都正常運行起來才能標記為部署成功;
  • OAM 平台應該支持組件施加多個 Traits,這些 Traits 可能是相同的類型;
  • OAM 對 Trait 的實現沒有任何限制,Trait 一般作用於應用的安裝和升級時;

分類

目前 Traits 主要分為三類:

  • Core Traits: core Traits 屬於 core.oam.dev 分組,是一些必要的運維特徵,所有 OAM 平台必須實現;
  • Standard Traits: standard Traits 屬於 standard.oam.dev 分組裡面,是一些常用的運維特徵,推薦 OAM 平台實現;
  • Extensions Traits: extension Traits 是自定義 Traits,其分組也是自定義,是平台特定的運維特徵(通常是特定 OAM 平台差異性)的體現。

定義

apiVersion,kind,metadata 和前面組件一致,不贅述,主要描述 Spec:

屬性 類型 必填 默認值 描述
appliesTo []string N [“*”] 該Trait可以應用的工作負載類型
properties []Properties N Trait的可配置參數,使用JSON Schema來表達。

例子

Manual Scaler(core)

apiVersion: core.oam.dev/v1alpha1
kind: Trait
metadata:
  name: ManualScaler
  annotations:
    version: v1.0.0
    description: "Allow operators to manually scale a workloads that allow multiple replicas."
spec:
  appliesTo:
    - core.oam.dev/v1alpha1.Server
    - core.oam.dev/v1alpha1.Worker
    - core.oam.dev/v1alpha1.Task
  properties: |
    {
      "$schema": "http://json-schema.org/draft-07/schema#",
      "type": "object",
      "required": ["replicaCount],
      "properties": {
        "replicaCount": {
          "type": "integer",
          "description": "the target number of replicas to scale a component to.",
          "minimum": 0
        }
      }
    }

上面是一個手動擴縮容服務的 Trait,只有一個參數就是 replicaCount。

總結

應用特徵聲明由 OAM 平台提供,透出應用特徵的可配置項,標明了可作用於的工作負載,可以看成函數定義,運維人員或者平台填寫實參之後,應用特徵就會按照聲明的功能運行起來,對綁定的組件起作用。

應用配置(Application Configuration)

應用配置主要是描述應用如何被部署的,一個組件可以部署到任意的運行時,我們稱一個組件的一次部署為實例,每次組件部署的時候必須有應用配置。

應用配置由應用運維管理,提供當前組件實例的信息:

  • 特定組件的基本信息:名字、版本、描述;
  • 組件及其相關組件定義 parameters 的賦值;
  • 組件要施加的 Trait 以及 Trait 的配置。

概念

實例與升級(Instances and upgrades)

一個實例是組件的可追溯部署,當組件部署時創建,後續該組件的升級都是修改該實例,回滾/重新部署都屬於升級,實例都會有名字方便引用。當一個實例首次創建時,處於初始發行 (release) 狀態,每次升級操作之後,一個新的發行就會創建。 

發行(Releases)

任何對組件本身或者其配置的變更都會創建一個新的發行,一個發行就是應用配置以及它對組件、應用特徵、應用邊界的定義,當一個發行被部署,對應的組件、應用特徵和應用邊界也會被部署。

基於該定義,平台需要保證以下變更語義:

  • 如果新的發行包含了舊發行不存在的組件,平台需要創建該組件;
  • 如果新的發行不包含舊發行存在的組件,平台需要刪除該組件;
  • 應用特徵和應用邊界與組件的變更語義一致。

運行時與應用配置(Runtime and Application Configuration)

一個組件可以部署到多個不同的運行時,在每個運行時中應用配置的實例與應用配置之間是 1:1 的關係,應用配置由應用運維管理,包含 3 個主要部分:

  • 參數:運維人員在部署時可以定義的參數;
  • 應用邊界列表:一組應用邊界列表,每個應用邊界定義對應的參數;
  • 組件實例定義:定義一個組件實例如何部署,這個定義本身有 3 個部分:

    • 組件參數的定義;
    • Traits 列表:每個 Trait 定義對應的參數;
    • 應用邊界列表:該組件應該部署到的應用邊界列表。

定義

apiVersion,kind,metadata 和前面組件一致,不贅述,主要描述 Spec:

屬性 類型 必填 默認值 描述
variables []Variable N 可以在參數值和屬性中引用的變量
scopes []Scope N 應用邊界定義
components []Component N 組件實例定義

variables 就是一個 k/v 對,一個集中的地方定義運維的變量,在運維配置的其他地方都可以用 fromVariable(VARNAME) 引用:

屬性 類型 必填 默認值 描述
name string Y 變量名字
value string Y 標量值

scopes 定義該運維配置將要創建的應用邊界,其定義為:

屬性 類型 必填 默認值 描述
name string Y 應用邊界名字
type string Y 應用邊界的GROUP/VERSION.KIND
properties Properties N 覆蓋邊界的參數

components 是組件實例定義,而不是組件定義:

屬性 類型 必填 默認值 描述
componentName string Y 組件名
instanceName string Y 組件實例名
parameterValues []ParameterValue N 覆蓋組件的參數
Traits []Trait N 指定組件實例綁定的Traits
applicationScopes []string N 指定組件運行的應用邊界

Trait 在這裏的定義是:

屬性 類型 必填 默認值 描述
name string Y Trait實例名
properties Properties N 覆蓋Trait的參數

例子

apiVersion: core.oam.dev/v1alpha1
kind: ApplicationConfiguration
metadata:
  name: my-app-deployment
  annotations:
    version: v1.0.0
    description: "Description of this deployment"
spec:
  variables:
    - name: VAR_NAME
      value: SUPPLIED_VALUE
  scopes:
    - name: core.oam.dev/v1alpha1.Network
      parameterValues:
        - name: PARAM_NAME
          value: SUPPLIED_VALUE
  components:
    - componentName: my-web-app-component
      instanceName: my-app-frontent
      parameterValues:
        - name: PARAMETER_NAME
          value: SUPPLIED_VALUE
        - name: ANOTHER_PARAMETER
          value: "[fromVariable(VAR_NAME)]"
      traits:
        - name: Ingress
          properties:
            CUSTOM_OBJECT:
              DATA: "[fromVariable(VAR_NAME)]"

總結

應用配置定義由運維人員或者 OAM 平台提供,描述應用的部署,可以看成是一個函數調用,運維人員或者 OAM 平台填寫實參之後,調用之前定義的組件、應用特徵、應用邊界等函數,這些實例一起作用對外提供應用服務。

工作負載類型(Workload Types)

Workload 類型和 Trait 一樣由平台提供,所以用戶可以查看平台提供哪些工作負載,對於平台用戶來說工作負載類型無法擴展,只能由平台開發者擴展提供,因此平台一定不允許用戶創建自定義的工作負載類型。

定義

apiVersion,kind,metadata 和前面組件類似,不贅述,這裏主要描述 Spec,定義組件如何使用工作負載類型,除此之外暴露了底層工作負載運行時的可配置參數:

屬性 類型 必填 默認值 描述
group string Y 該工作負載類型所屬的group
names Names Y 該工作負載類型的關聯名字信息
settings []Setting N 該工作負載的設置選項

Names 就是描述了對應類型的不同形式名字引用:

屬性 類型 必填 默認值 描述
kind string Y 工作負載類型的正確引用名字,比如Singleton
singular string N 單數形式的可讀名字,比如singleton
plural string N 複數形式的可讀名字,比如singletons

Setting 描述工作負載可配置部分,類似前面組件的 Parameters,都是 schema:

屬性 類型 必填 默認值 描述
name string Y 配置名,每個workload類型必須唯一
description string N 配置說明
type string Y 配置類型
required bool N false 是否必須提供
default indicated by type N 默認值

價值

通過上面的介紹,我們了解了 OAM Spec 裏面的基本概念和定義,以及如何使用它們來描述應用交付和運維流程。然而,OAM 能給我們帶來什麼樣的價值呢?我們評判一個好的架構體系,不僅是因為它在技術上更先進,更主要的是它能夠解決一些實際問題,為用戶帶來價值。所以,接下來我們將總結一下這方面的內容。

OAM 的價值要從下往上三個層面來說起。

1. 從基礎設施層面

基礎設施,指的是像 K8s 這類的提供基礎服務能力與抽象的一層服務體系。拿 K8s 來說,它提供了許多種類的基礎服務和強大的擴展能力來靈活擴展其他基礎服務。

但是,使用基礎設施的運維人員很快就發現 K8s 存在一個問題:缺乏統一的機制來註冊和管理自定義擴展能力。這些擴展能力的表達方式不夠統一,有些是 CRD、有些是 annotation、有些是 Config…

這種亂象使得基礎設施用戶不知道平台上都提供了哪些能力,不知道怎麼使用這些能力,更不知道這些能力互相之間的兼容組合關係。

OAM 提供了抽象(如 Workload/Trait 等)來統一定義和管理這些能力。有了 OAM,各平台實現就有了統一的標準規範去透出公共的或差異化的能力:公共的基礎服務像容器部署、監控、日誌、灰度發布;差異化的、高級複雜的能力像 CronHPA(周期性定時變化的強化版 HPA)。

2. 從應用運維者層面

應用運維,指的是像給應用加上網絡接入、複雜均衡、彈性伸縮、甚至是建站等運維操作。但是,運維的一個痛點就是原來這些能力並不是跨平台的:這導致在不同平台、不同環境下去部署和運維應用的操作,是不互通和不兼容的。

上面這個問題,是客戶應用、尤其是傳統 ERP 應用上雲的一大阻礙。我們做 OAM 的一個初衷,就是通過一套標準定義,讓不同的平台實現也通過統一的方式透出。我們希望:哪怕一個應用不是生在雲上、長在雲上,也能夠趕上這趟通往雲原生未來的列車,擁抱雲帶來的變化和紅利!

OAM 提供的抽象和模型,是我們通往統一、標準的應用架構的強有力工具。這些標準能力以後都會通過 OAM 輸出,讓運維人員輕易去實現跨平台部署。

3. 從應用開發者層面

應用開發,指的就是業務邏輯開發,這是業務產生價值的核心位置。

也正因如此,我們希望,應用開發者能夠專註於業務開發,而不需要關心運維細節。但是,K8s 提供的 API,並沒有很好地分離開發和運維的關注點,開發和運維之間需要來回溝通以避免產生誤解和衝突。

OAM 分離了開發和運維的關注點,很好地解決了以上問題,讓整個發布流程更加連貫、高效。

下一步

目前,OAM 已經在阿里雲 EDAS 等多個項目中進行了數月的內部落地嘗試。我們希望通過一套統一、標準的應用定義體系,承載雲應用管理項目產品與外部資源關係的高效管理體驗,並將這種體驗統一帶給了基於 Function、ECS、Kubernetes 等不同運行時的應用管理流程;通過應用特徵系統,將多個阿里雲獨有的能力進行了模塊化,大大提高了阿里雲基礎設施能力的交付效率。

經過了前一段努力的鋪墊,我們也慢慢明確了接下來的工作方向:

  • 將接入更多的雲產品服務,為用戶將跨平台應用交付的能力最大化;
  • 提供 OAM framework 等工具和框架,幫助新的 OAM 平台開發者去快速、簡單地搭建 OAM 服務,接入 OAM 標準;
  • 推動開源生態建設,以標準化的方式幫助“應用”高效和高質量地交付到任何平台上去。

社區共建

為了能夠讓社區更加高效、健康的運轉下去,我們非常期待得到您的反饋,並與大家密切協作,針對 Kubernetes 和任意雲環境打造一個簡單、可移植、可復用的應用模型。參与方式:

  • 通過 Gitter 直接參与討論:;
  • 選擇釘釘掃碼進入 OAM 項目中文討論群。

(****釘釘掃碼加入交流群****)

歡迎你與我們一起共建這個全新的應用管理生態!

“ 阿里巴巴雲原生微信公眾號(ID:Alicloudnative)關注微服務、Serverless、容器、Service Mesh等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的技術公眾號。”

更多相關信息,請關注。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

USB CONNECTOR 掌控什麼技術要點? 帶您認識其相關發展及效能

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

以太網驅動的流程淺析(一)-Ifconfig主要流程【原創】

以太網驅動的流程淺析(一)-Ifconfig主要流程

Author:張昺華
Email:920052390@qq.com
Time:2019年3月23日星期六

此文也在我的個人公眾號以及《Linux內核之旅》上有發表:

很喜歡一群人在研究技術,一起做有意思的東西,一起分享技術帶給我們的快樂,也希望中國有更多的人熱愛技術,喜歡一起研究、分享技術,然後可以一起用我們的技術來做一些好玩的東西,可以為這個社會創造一些東西來改善人們的生活。
如下是本人調試過程中的一點經驗分享,以太網驅動架構畢竟涉及的東西太多,如下僅僅是針對加載流程和圍繞這個問題產生的分析過程和驅動加載流程部分,並不涉及以太網協議層的數據流程分析。

【硬件環境】 Imx6ul

【Linux kernel版本】 Linux4.1.15

【以太網phy】 Realtek8201f

一個以太網的案例來講述Ifconfig

1. 問題描述

【問題】

機器通過usb方式下載了mac地址后,發現以太網無法正常使用,敲命令 ifconfig eth0 up出現:ifconfig: SIOCSIFFLAGS: No such device,而對於沒有下載以太網mac address的機器表現均正常。調試過程中發現在以太網控制器代碼中加入一些printk,不正常的機器又正常了,打印的位置不同,機器的以太網有時會正常,有時會異常,十分詭異。

2. 原因分析

【根本原因】

reset時序問題導致,phy reset的時間不滿足時序要求。如下圖,如果硬件接了reset引腳,應滿足時序要求在reset保持10ms有效電平后,還必須維持至少150ms才可以訪問phy register,也就是reset要在B點之後才可以正常通過MDC/MDIO來訪問phy register。如果是不使用硬件reset,使用軟件reset方式,那也要至少在A點,也就是在reset維持10ms有效電平后,再維持3.5個clk才能正常訪問phy register。

那為什麼下載了mac地址后才異常呢?不下載的又正常呢?

【原因分析】

freescale控制器獲取mac address流程如下:
1)模塊化參數設置,如果沒有跳到步驟2;
2)device tree中設置,如果沒有跳到步驟3;
3)from flash / fuse / via platform data,如果沒有跳到步驟4;
4)FEC mac registers set by bootloader===》即靠usb方式下載mac address ,如果沒有跳到步驟5;
5)靠kernel算一個隨機數mac address出來,然後寫入mac

那為什麼下載了mac地址后才異常呢?
下了mac后,會執行步驟4,不會執行步驟5,此時目前的代碼不滿足150ms的時序要求,無法訪問phy register,
導致phy_id獲取不到,因此phy_device也不會創建

那為什麼不下載的又正常呢?
不下載mac address,會執行步驟5 ,步驟5中調用了函數eth_hw_addr_random
剛好滿足了150ms的時序要求,所以才可以正常

跟入代碼eth_hw_addr_random看下

繼續看:

最終調用了kernel提供的獲取隨機數的一個函數,這塊代碼比較多就不繼續追下去了。

所以這塊步驟五的代碼剛剛好好在這個硬件條件下,恰巧滿足了150ms的reset時序要求,所以以太網才可以正常。

3. 以太網流程分析跟蹤

3.1 Ifconfig主要流程

回歸主題,根據這個ifconfig失敗的現象,我們追蹤一下code:
ifconfig: SIOCSIFFLAGS: No such device,既然出現了這個問題log,我們就從應用層的log入手,首先我們使用strace命令來追蹤下系統調用,以便於我們追蹤內核代碼實現。
strace ifconfig eth0 up跟蹤一下

可以發現主要是ioctl的操作,SIOCSIFFLAGS,然後我們需要了解下這個宏的意思,說白了就是設置各種flag,靠ioctl第三個參數把所需要的動作flag傳入,比如說此時要對eth0進行up動作,那麼就傳入IFF_UP,例如:
struct ifreq ifr;

我們看這些主要是想知道為什麼會打印這個log:
ifconfig: SIOCSIFFLAGS: No such device
那麼內核中又是對ioctl做了什麼動作呢?因為strace命令讓我們知道了系統調用調用函數,我們可以在kernel中直接搜索SIOCSIFFLAGS,或者去以太網驅動net目錄下直接搜索更快。最終我搜到了,路徑是:net/ipv4/devinet.c
我們可以看到內核的宏定義:

查看devinet.c的代碼,我們找到了那個宏,也就是做devinet_ioctl函數中,這也就是應用層的ioctl最終的實現函數,然後我們在裏面加一些打印,

通過打印結果我們可以確認是這個函數devinet_ioctl為應用層的ioctl的實現函數,因為你在kernel中搜SIOCSIFFLAGS宏的話會有很多地方出現的,所以我們需要確認我們找的函數
沒問題:

看到這裏返回值ret是-19,那麼我們繼續順着追蹤下去,上代碼:
net/core/dev.c

繼續追蹤:net/core/dev.c

因此我們可以看到返回值-19就是如下代碼產生的

因此我們需要追蹤__dev_open函數,繼續看代碼:

通過調試,比如說加打印,或者是經驗我們可以推斷出是這裏返回的-19,那麼這個ndo_open又是在哪裡回調的呢?

我們可以看到ops這個結構的結構體
struct net_device dev
const struct net_device_ops
ops = dev->netdev_ops;

這裏熟悉驅動的朋友應該可以猜到這在在freescale的以太網控制器驅動中一定有它的實現
net_device_ops就是kernel提供給drvier操作net_device的一些操作方法,具體實現自然由相應廠商的driver自己去實現。
路徑:drivers/net/Ethernet/freescale/fec_main.c

我們可以在這個fec_enet_open函數中加入dump_stack來看下整個調用情況
我們打出kernel的dump_stack信息來看:

這個調用過程就是應用層ioctl一直到kernel最底層fec_enet_open的過程。
應用代碼這樣:

總體流程:kill() -> kill.S -> swi陷入內核態 -> 從sys_call_table查看到sys_kill -> ret_fast_syscall -> 回到用戶態執行kill()下一行代碼
Ioctl《==ret_fast_syscall 《==SyS_ioctl《==do_vfs_ioctl《==vfs_ioctl《==sock_ioctl《==
devinet_ioctl《==dev_change_flags《==__dev_change_flags《==__dev_open《==fec_enet_open
我附上每個函數的代碼:
如果大家想看系統調用流程的話,參考這篇,我就不做這塊的說明了:
Linux系統調用(syscall)原理
http://gityuan.com/2016/05/21/syscall/
Arm Linux系統調用流程詳細解析
https://www.cnblogs.com/cslunatic/p/3655970.html

4. 網址分享

http://stackoverflow.com/questions/5308090/set-ip-address-using-siocsifaddr-ioctl
http://www.ibm.com/support/knowledgecenter/ssw_aix_72/com.ibm.aix.commtrf2/ioctl_socket_control_operations.htm
https://lkml.org/lkml/2017/2/3/396

http://www.latelee.org/programming-under-linux/linux-phy-driver.html
Linux PHY幾個狀態的跟蹤
http://www.latelee.org/programming-under-linux/linux-phy-state.html
第十六章PHY -基於Linux3.10
https://blog.csdn.net/shichaog/article/details/44682931

“`

End

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Spring Cloud Alibaba(四)實現Dubbo服務消費

本項目演示如何使用 Spring Cloud Alibaba 完成 Dubbo 的RPC調用。

Spring Cloud與Dubbo

  • Spring Cloud是一套完整的微服務架構方案

  • Dubbo是國內目前非常流行的服務治理與RPC實現方案

由於Dubbo在國內有着非常大的用戶群體,但是其周邊設施與組件相對來說並不那麼完善(比如feign,ribbon等等)。很多開發者使用Dubbo,又希望享受Spring Cloud的生態,因此也會有一些Spring Cloud與Dubbo一起使用的案例與方法出現。

Spring Cloud Alibaba的出現,實現了Spring Cloud與Dubbo的完美融合。在之前的教程中,我們已經介紹過使用Spring Cloud Alibaba中的Nacos來作為服務註冊中心,並且在此之下可以如傳統的Spring Cloud應用一樣地使用Ribbon或Feign來實現服務消費。這篇,我們就來繼續說說Spring Cloud Alibaba 下額外支持的RPC方案:Dubbo

代碼實現

我們通過一個簡單的例子,使用Nacos做服務註冊中心,利用Dubbo來實現服務提供方與服務消費方。這裏省略Nacos的安裝與使用,下面就直接進入Dubbo的使用步驟。

定義 Dubbo 服務接口

創建 ali-nacos-dubbo-api 工程

Dubbo 服務接口是服務提供方與消費方的遠程通訊契約,通常由普通的 Java 接口(interface)來聲明,如 HelloService 接口:

public interface HelloService {
    String hello(String name);
}

為了確保契約的一致性,推薦的做法是將 Dubbo 服務接口打包在jar包中,如以上接口就存放在 ali-nacos-dubbo-api 之中。 對於服務提供方而言,不僅通過依賴 artifact 的形式引入 Dubbo 服務接口,而且需要將其實現。對應的服務消費端,同樣地需要依賴該 artifact, 並以接口調用的方式執行遠程方法。接下來進一步討論怎樣實現 Dubbo 服務提供方和消費方。

實現 Dubbo 服務提供方

創建 ali-nacos-dubbo-provider,端口:9001 工程

pom.xml配置

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud-alibaba</artifactId>
        <groupId>com.easy</groupId>
        <version>1.0.0</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>

    <artifactId>ali-nacos-dubbo-provider</artifactId>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-actuator</artifactId>
        </dependency>

        <!-- API -->
        <dependency>
            <groupId>com.easy</groupId>
            <artifactId>ali-nacos-dubbo-api</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!--dubbo-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-dubbo</artifactId>
        </dependency>

        <!--nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

bootstrap.yaml配置

dubbo:
  scan:
    # dubbo 服務掃描基準包
    base-packages: com.easy.andProvider.service

  #Dubbo 服務暴露的協議配置,其中子屬性 name 為協議名稱,port 為協議端口( -1 表示自增端口,從 20880 開始)
  protocol:
    name: dubbo
    port: -1

  #Dubbo 服務註冊中心配置,其中子屬性 address 的值 “spring-cloud://localhost”,說明掛載到 Spring Cloud 註冊中心
  registry:
    address: spring-cloud://localhost

spring:
  application:
    # Dubbo 應用名稱
    name: ali-nacos-dubbo-provider
  main:
    allow-bean-definition-overriding: true
  cloud:
    # Nacos 服務發現與註冊配置
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

Dubbo Spring Cloud 繼承了 Dubbo Spring Boot 的外部化配置特性,也可以通過標註 @DubboComponentScan 來實現基準包掃描。

實現 Dubbo 服務

HelloService 作為暴露的 Dubbo 服務接口,服務提供方 ali-nacos-dubbo-provider 需要將其實現:

package com.easy.andProvider.service;

import com.easy.and.api.service.HelloService;
import org.apache.dubbo.config.annotation.Service;

@Service
public class HelloServiceImpl implements HelloService {
    @Override
    public String hello(String name) {
        return "你好 " + name;
    }
}

import org.apache.dubbo.config.annotation.Service 是 Dubbo 服務註解,僅聲明該 Java 服務實現為 Dubbo 服務

貼上啟動類代碼:

@EnableDiscoveryClient
@EnableAutoConfiguration
public class AndProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(AndProviderApplication.class);
    }
}

實現 Dubbo 服務消費方

創建 ali-nacos-dubbo-consumer,端口:9103 工程

pom.xml依賴

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud-alibaba</artifactId>
        <groupId>com.easy</groupId>
        <version>1.0.0</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>

    <artifactId>ali-nacos-dubbo-consumer</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>com.easy</groupId>
            <artifactId>ali-nacos-dubbo-api</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-dubbo</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

yaml配置文件

dubbo:
  registry:
    address: spring-cloud://localhost
  cloud:
    subscribed-services: ali-nacos-dubbo-provider

spring:
  application:
    name: ali-nacos-dubbo-consumer
  main:
    allow-bean-definition-overriding: true
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

實現 Dubbo 服務消費方

HomeController.java

package com.easy.andConsumer;

import com.easy.and.api.service.HelloService;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class HomeController {

    @Reference
    HelloService helloService;

    @GetMapping("/hello")
    public String hello(String name) {
        return helloService.hello("雲天");
    }
}

AndConsumerApplication.java啟動類

package com.easy.andConsumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class AndConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(AndConsumerApplication.class, args);
    }
}

使用示例

示例關聯項目

本示例我們創建了三個項目實現

  • ali-nacos-dubbo-api:定義Dubbo服務接口工程

  • ali-nacos-dubbo-provider:Dubbo服務提供方並向nacos註冊服務,服務名:ali-nacos-dubbo-provider,端口:9001

  • ali-nacos-dubbo-consumer:Dubbo服務消費方並向nacos註冊服務,服務名:ali-nacos-dubbo-consumer,端口:9103

運行示例測試

首先要啟動服務註冊中心 nacos、ali-nacos-dubbo-provider服務及ali-nacos-dubbo-consumer服務

  • 訪問服務消費方地址: http://localhost:9103/hello

返回

你好 雲天

或者你也可以通過 curl 命令執行 HTTP GET 方法

$curl http://127.0.0.1:9103/hello

HTTP 響應為:

你好 雲天

以上結果說明應用 ali-nacos-dubbo-consumer 通過消費 Dubbo 服務,返回服務提供方 ali-nacos-dubbo-provider 運算后的內容。

以上我們完成了 Dubbo 服務提供方和消費方的入門運用,源代碼請直接參考模塊:

資料

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

【控制系統数字仿真與CAD】實驗三:離散相似法数字仿真

一、實驗目的
  1. 了解離散相似法的基本原理
  2. 掌握離散相似法仿真的基本過程
  3. 應用離散相似法仿真非線性系統
  4. MATLAB實現離散相似法的非線性系統仿真
  5. 掌握SIMULINK仿真方法,應用於非線性系統的仿真,並對實驗結果進行分析比較

二、實驗原理

  在ASR的輸出增加限幅裝置(飽和非線性,飽和界為c=8 )。 Ce=0.031,其它參數不變。輸入為單位階躍,用離散相似法求系統各環節的輸出。

  要求:採用零階保持器和一階保持器離散化系統,分別完成本實驗。

  1、各環節的參數:

  由5個典型環節組成:

  A=[0 0 1 1 0];

  B=[tn ti Ts Tl Tm*Ce];
  C=[Kn Ki Ks 1/R R];
  D=[Kn*tn Ki*ti 0 0 0];

  還有一飽和非線性環節:c=8;

  2、各環節的離散化係數矩陣

 3、各環節的輸入作用

   

    (1)、u(n)可通過聯接矩陣直接求得: 

      

      u(n)=[u1(n), u2(n),…,un(n)]為各環節的輸入量, n為環節數。 Y(n)=[Y1(n), Y2(n),…,Yn(n)]為各環節的輸出量, r為外中參考輸入量。

    (2)U(n)由近似表達式求得:
      

    (3)、u(n+1)用折線法近似求得:

      

 

  4、狀態和輸出計算

    (1)、一階保持器
      X=FI’.*X+FIM’.*Uk+FIJ’.*Udot;
      Y=FIC’.*X+FID’.*Uf;
    (2)、零階保持器
      X=FI’.*X+FIM’.*Uk;
      Y=FIC’.*X+FID’.*Uf;

  5、飽和非線性環節

  看作環節1ASR)的一部分。建立satur.m文件:

function [uo]=satur(ui,c)
    if (abs(ui)<=c)
        uo=ui;
    elseif ( ui > c )
        uo = c;
    else
        uo=-c;
    end
end

 三、實驗過程

  1、新建腳本文件,命名為satur.m

function [uo]=satur(ui,c)
    if (abs(ui)<=c)
        uo=ui;
    elseif ( ui > c )
        uo = c;
    else
        uo=-c;
    end
end

  2、新建腳本文件,命名為test3.m

  完整代碼:

clc;
clear;
% ******  各環節參數  ****** %
Kn=26.7;
tn=0.03;
Ki=0.269;
ti=0.067;
Ks=76;
Ts=0.00167;
R=6.58;
T1=0.018;
Tm=0.25;
Ce=0.031;
Alpha=0.00337;
Beta=0.4;
A=[0 0 1 1 0];
B=[tn ti Ts T1 Tm*Ce];
C=[Kn Ki Ks 1/R R];
D=[Kn*tn Ki*ti 0 0 0];
c=8;
r=1;
W=[0 0 0 0 -Alpha;
    1 0 0 -Beta 0;
    0 1 0 0 0;
    0 0 1 0 -Ce;
    0 0 0 1 0];
W0=[1 0 0 0 0]';
h=0.001;
t_end=0.5;
t=0:h:t_end;
n=length(t);
% ******  各環節離散化係數  ****** %
block_num=5;
for k=1:block_num
    if(A(k)==0)
        FI(k)=1;
        FIM(k)=h*C(k)/B(k);
        FIJ(k)=h*h*C(k)/B(k)/2;
        FIC(k)=1;
        FID(k)=0;
        if(D(k)~=0)
            FID(k)=D(k)/B(k);
        end
    else
        FI(k)=exp(-h*A(k)/B(k));
        FIM(k)=(1-FI(k))*C(k)/A(k);
        FIJ(k)=h*C(k)/A(k)-FIM(k)*B(k)/A(k);
        FIC(k)=1;
        FID(k)=0;
        if(D(k)~=0)
            FIC(k)=C(k)/D(k)-A(k)/B(k);
            FID(k)=D(k)/B(k);
        end
    end
end

Y0=[0 0 0 0 0]';
Y=Y0;
X=zeros(block_num,1);
result1=Y;
Uk=zeros(block_num,1);
Ub=Uk;

for m=1:(n-1)
    Ub=Uk;
    Uk=W*Y+W0*r;
    Uf=2*Uk-Ub;
    Udot=(Uk-Ub)/h;
    
    %******  零階保持器  ******%
    X=FI'.*X+FIM'.*Uk;
    Y=FIC'.*X+FID'.*Uf;
    
    Y(1)=satur(Y(1),c);
    result1=[result1,Y];
end

Y0=[0 0 0 0 0]';
Y=Y0;
X=zeros(block_num,1);
result2=Y;
Uk=zeros(block_num,1);
Ub=Uk;
for m=1:(n-1)
    Ub=Uk;
    Uk=W*Y+W0*r;
    Uf=2*Uk-Ub;
    Udot=(Uk-Ub)/h;
    
    %******  一階保持器  ******%
    X=FI'.*X+FIM'.*Uk + FIJ'.*Udot;
    Y=FIC'.*X + FID'.*Uf;
    
    Y(1)=satur(Y(1),c);
    result2=[result2,Y];
end

plot(t,result1(5,:),’-.’,t,result2(5,:),’–‘,t,ScopeData.signals.values,’k’);
legend(‘零階保持器’,’一階保持器’,’Simulink’);

  3、在Simulink中繪製仿真圖

 

 注意:Simulink中的變量名和工作區變量關聯方法請點擊:https://www.cnblogs.com/KaifengGuan/p/11942615.html

四、實驗結果

 

 

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

【其他文章推薦】

USB CONNECTOR 掌控什麼技術要點? 帶您認識其相關發展及效能

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

Docker-Compose基礎與實戰,看這一篇就夠了

what & why

Compose 項目是 Docker 官方的開源項目,負責實現對 Docker 容器集群的快速編排。使用前面介紹的Dockerfile我們很容易定義一個單獨的應用容器。然而在日常開發工作中,經常會碰到需要多個容器相互配合來完成某項任務的情況。例如要實現一個 Web 項目,除了 Web 服務容器本身,往往還需要再加上後端的數據庫服務容器;再比如在分佈式應用一般包含若干個服務,每個服務一般都會部署多個實例。如果每個服務都要手動啟停,那麼效率之低、維護量之大可想而知。這時候就需要一個工具能夠管理一組相關聯的的應用容器,這就是Docker Compose。
Compose有2個重要的概念

  • 項目(Project):由一組關聯的應用容器組成的一個完整業務單元,在 docker-compose.yml 文件中定義。
  • 服務(Service):一個應用的容器,實際上可以包括若干運行相同鏡像的容器實例。

docker compose 安裝與卸載

安裝

二進制包在線安裝

curl -L https://github.com/docker/compose/releases/download/1.25.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose 

sudo chmod +x /usr/local/bin/docker-compose

這個方法現在基本行不通,下載太慢了,不推薦使用。

二進制包離線安裝

https://github.com/docker/compose/releases/download/1.25.0/docker-compose-Linux-x86_64下載對應的安裝包,比如我下載了Linux-x86_64的。

將下載好的安裝包剪切到/usr/local/bin/docker-compose目錄下
mv /app/download/docker-compose-Linux-x86_64 /usr/local/bin/docker-compose

添加執行權限
sudo chmod +x /usr/local/bin/docker-compose

pip安裝

  • 先安裝好pip工具
#安裝依賴 
yum -y install epel-release 
#安裝PIP 
yum -y install python-pip 
#升級PIP 
pip install --upgrade pip
  • 驗證pip 版本
[root@tymonitor bin]# pip --version
pip 8.1.2 from /usr/lib/python2.7/site-packages (python 2.7)
  • 安裝docker compose
    pip install -U docker-compose==1.25.0

  • 驗證docker compose版本
[root@tymonitor bin]# docker-compose --version
docker-compose version 1.25.0, build b42d419

安裝補全插件

curl -L https://raw.githubusercontent.com/docker/compose/1.25.0/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose

卸載

二進制卸載

rm /usr/local/bin/docker-compose

pip卸載

pip uninstall docker-compose

docker compose 重要命令

命令選項

  • -f, –file FILE 指定使用的 Compose 模板文件,默認為 docker-compose.yml,可以多次指定。
  • -p, –project-name NAME 指定項目名稱,默認將使用所在目錄名稱作為項目名。
  • –x-networking 使用 Docker 的可拔插網絡後端特性
  • –x-network-driver DRIVER 指定網絡後端的驅動,默認為 bridge
  • –verbose 輸出更多調試信息。
  • -v, –version 打印版本並退出。

常用&重要命令

  • config
    驗證 Compose 文件格式是否正確,若正確則显示配置,若格式錯誤显示錯誤原因。
    如:docker-compose -f skywalking.yml config
    此命令不會執行真正的操作,而是显示 docker-compose 程序解析到的配置文件內容:

  • images
    列出 Compose 文件中包含的鏡像。如docker-compose -f skywalking.yml images

  • ps
    列出項目中目前的所有容器。如docker-compose -f skywalking.yml ps

  • build
    構建(重新構建)項目中的服務容器。如:docker-compose -f skywalking.yml build,一般搭配自定義鏡像,比如編寫的Dockfile,功能類似於docker build .

  • up
    該命令十分強大(重點掌握),它將嘗試自動完成包括構建鏡像,(重新)創建服務,啟動服務,並關聯服務相關容器的一系列操作。如docker-compose -f skywalking.yml up。默認情況,docker-compose up 啟動的容器都在前台,控制台將會同時打印所有容器的輸出信息,可以很方便進行調試。

    如果使用docker-compose up -d將會在後台啟動並運行所有的容器。一般推薦生產環境下使用該選項。
    默認情況,如果服務容器已經存在,docker-compose up 將會嘗試停止容器,然後重新創建(保持使用 volumes-from 掛載的卷),以保證新啟動的服務匹配 docker-compose.yml 文件的最新內容。如果用戶不希望容器被停止並重新創建,可以使用 docker-compose up --no-recreate。這樣將只會啟動處於停止狀態的容器,而忽略已經運行的服務。如果用戶只想重新部署某個服務,可以使用 docker-compose up --no-deps -d <SERVICE_NAME> 來重新創建服務並後台停止舊服務,啟動新服務,並不會影響到其所依賴的服務。此命令有如下選項:
    ①:-d 在後台運行服務容器。
    ②:--no-color 不使用顏色來區分不同的服務的控制台輸出。
    ③:--no-deps 不啟動服務所鏈接的容器。
    ④:--force-recreate 強制重新創建容器,不能與 –no-recreate 同時使用。
    ⑤:--no-recreate 如果容器已經存在了,則不重新創建,不能與 –force-recreate 同時使用。
    ⑥:--no-build 不自動構建缺失的服務鏡像。
    ⑦:-t, --timeout TIMEOUT 停止容器時候的超時(默認為 10 秒)。

  • down
    此命令停止用up命令所啟動的容器並移除網絡,如docker-compose -f skywalking.yml down

  • stop
    格式為 docker-compose stop [options] [SERVICE...]
    停止已經處於運行狀態的容器,但不刪除它。通過 docker-compose start 可以再次啟動這些容器,如果不指定service則默認停止所有的容器。如docker-compose -f skywalking.yml stop elasticsearch
    選項:
    -t, --timeout TIMEOUT 停止容器時候的超時(默認為 10 秒)。

  • start
    啟動已經存在的服務容器。用法跟上面的stop剛好相反,如docker-compose -f skywalking.yml start elasticsearch

  • restart
    重啟項目中的服務。用法跟上面的stop,start一樣

  • logs
    格式為docker-compose logs [options] [SERVICE...]
    查看服務容器的輸出。默認情況下,docker-compose 將對不同的服務輸出使用不同的顏色來區分。可以通過 –no-color 來關閉顏色。該命令在調試問題的時候十分有用。如docker-compose -f skywalking.yml logs 查看整體的日誌,docker-compose -f skywalking.yml logs elasticsearch 查看單獨容器的日誌

docker compose 模板文件

模板文件是使用 Compose 的核心,涉及到的指令關鍵字也比較多。本文主要列出幾個常見&重要的指令,其他指令大家可以自行百度。
默認的模板文件名稱為 docker-compose.yml,格式為 YAML 格式。

version: '3'
services:
  elasticsearch:
    image: elasticsearch:6.8.5
    container_name: elasticsearch
    restart: always
    volumes:
      - /app/skywalking/elasticsearch/data:/usr/share/elasticsearch/data:rw
      - /app/skywalking/elasticsearch/conf/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
      - /app/skywalking/elasticsearch/conf/jvm.options:/usr/share/elasticsearch/config/jvm.options
      - /app/skywalking/elasticsearch/logs:/usr/share/elasticsearch/logs:rw
    environment:
      - TZ=Asia/Shanghai
      - xpack.monitoring.enabled=false
      - xpack.watcher.enabled=false
    ports:
      - "9200:9200"
      - "9300:9300"

注意每個服務都必須通過 image 指令指定鏡像或 build 指令(需要 Dockerfile)等來自動構建生成鏡像。如果使用 build 指令,在 Dockerfile 中設置的選項(例如:CMD, EXPOSE, VOLUME, ENV 等) 將會自動被獲取,無需在 docker-compose.yml 中重複設置。

常用&重要命令

  • images
    指定為鏡像名稱或鏡像 ID。如果鏡像在本地不存在,Compose 將會嘗試拉取這個鏡像。
image: apache/skywalking-oap-server:6.5.0
image: apache/skywalking-ui:6.5.0
  • ports
    暴露端口信息。
    使用宿主端口:容器端口 (HOST:CONTAINER) 格式,或者僅僅指定容器的端口(宿主將會隨機選擇端口)都可以,端口字符串都使用引號包括起來的字符串格式。
ports: 
    - "3000" 
    - "8080:8080" 
    - "127.0.0.1:8001:8001"
  • volumes
    數據卷所掛載路徑設置。可以設置為宿主機路徑(HOST:CONTAINER)或者數據卷名稱(VOLUME:CONTAINER),並且可以設置訪問模式 (HOST:CONTAINER:ro)。
volumes:
      - /app/skywalking/elasticsearch/data:/usr/share/elasticsearch/data:rw
      - conf/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
version: "3"
services:
  my_src:
    image: mysql:8.0
    volumes:
      - mysql_data:/var/lib/mysql
volumes:
  mysql_data:
  • ulimits
    指定容器的 ulimits 限制值。
    例如,指定最大進程數為 65535,指定文件句柄數為 20000(軟限制,應用可以隨時修改,不能超過硬限制) 和 40000(系統硬限制,只能 root 用戶提高)。
ulimits:
   nproc: 65535
   nofile:
     soft: 20000
     hard: 40000
  • depends_on
    解決容器的依賴、啟動先後的問題。以下例子中會先啟動 redis mysql 再啟動 web
version: '3'
services:
  web:
    build: .
    depends_on:
      - db
      - redis     
  redis:
    image: redis    
  db:
    image: mysql
  • environment
    設置環境變量。你可以使用數組或字典兩種格式。
environment:
      SW_STORAGE: elasticsearch
      SW_STORAGE_ES_CLUSTER_NODES: elasticsearch:9200
      
environment:
      - SW_STORAGE= elasticsearch
      - SW_STORAGE_ES_CLUSTER_NODES=elasticsearch:9200
  • restart
    指定容器退出后的重啟策略為始終重啟。該命令對保持服務始終運行十分有效,在生產環境中推薦配置為 always 或者 unless-stopped
    restart: always

docker-compose 實戰

首先我需要推薦兩件事:

  • 配置docker加速鏡像
    創建或修改/etc/docker/daemon.json
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
    "registry-mirrors": [
        "https://hub-mirror.c.163.com",
        "https://mirror.ccs.tencentyun.com",
        "https://reg-mirror.qiniu.co"
    ]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
  • 給你的ide工具裝上docker插件

本次實戰我們以docker-compose部署skywalking為例。編寫skywalking.yml,內容如下。

version: '3.3'
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:6.8.5
    container_name: elasticsearch
    restart: always
    ports:
      - 9200:9200
      - 9300:9300
    environment:
      discovery.type: single-node
    ulimits:
      memlock:
        soft: -1
        hard: -1
  oap:
    image: skywalking/oap
    container_name: oap
    depends_on:
      - elasticsearch
    links:
      - elasticsearch
    restart: always
    ports:
      - 11800:11800
      - 12800:12800
    environment:
      SW_STORAGE: elasticsearch
      SW_STORAGE_ES_CLUSTER_NODES: elasticsearch:9200
  ui:
    image: skywalking/ui
    container_name: ui
    depends_on:
      - oap
    links:
      - oap
    restart: always
    ports:
      - 8080:8080
    environment:
      SW_OAP_ADDRESS: oap:12800

部署完成后將其上傳至服務器,執行docker-compose -f /app/skywalking.yml up -d即可。

個人公眾號:JAVA日知錄 ,

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

USB CONNECTOR 掌控什麼技術要點? 帶您認識其相關發展及效能

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

設計模式之美學習(九):業務開發常用的基於貧血模型的MVC架構違背OOP嗎?

我們都知道,很多業務系統都是基於 MVC 三層架構來開發的。實際上,更確切點講,這是一種基於貧血模型的 MVC 三層架構開發模式。

雖然這種開發模式已經成為標準的 Web 項目的開發模式,但它卻違反了面向對象編程風格,是一種徹徹底底的面向過程的編程風格,因此而被有些人稱為反模式(anti-pattern)。特別是領域驅動設計Domain Driven Design,簡稱 DDD)盛行之後,這種基於貧血模型的傳統的開發模式就更加被人詬病。而基於充血模型的 DDD 開發模式越來越被人提倡。

基於上面的描述,我們先搞清楚下面幾個問題:

  • 什麼是貧血模型?什麼是充血模型?
  • 為什麼說基於貧血模型的傳統開發模式違反 OOP?
  • 基於貧血模型的傳統開發模式既然違反 OOP,那又為什麼如此流行?
  • 什麼情況下我們應該考慮使用基於充血模型的 DDD 開發模式?

什麼是基於貧血模型的傳統開發模式?

對於大部分的後端開發工程師來說,MVC 三層架構都不會陌生。

MVC 三層架構中的 M 表示 ModelV 表示 ViewC 表示 Controller。它將整個項目分為三層:展示層、邏輯層、數據層。MVC 三層開發架構是一個比較籠統的分層方式,落實到具體的開發層面,很多項目也並不會 100% 遵從 MVC 固定的分層方式,而是會根據具體的項目需求,做適當的調整。

比如,現在很多 Web 或者 App 項目都是前後端分離的,後端負責暴露接口給前端調用。這種情況下,我們一般就將後端項目分為 Repository 層、Service 層、Controller 層。其中,Repository 層負責數據訪問,Service 層負責業務邏輯,Controller 層負責暴露接口。當然,這隻是其中一種分層和命名方式。不同的項目、不同的團隊,可能會對此有所調整。不過,萬變不離其宗,只要是依賴數據庫開發的 Web 項目,基本的分層思路都大差不差。

再來看一下,什麼是貧血模型?

目前幾乎所有的業務後端系統,都是基於貧血模型的。舉一個簡單的例子來解釋一下。

////////// Controller+VO(View Object) //////////
public class UserController {
  private UserService userService; //通過構造函數或者IOC框架注入
  
  public UserVo getUserById(Long userId) {
    UserBo userBo = userService.getUser(userId);
    UserVo userVo = [...convert userBo to userVo...];
    return userVo;
  }
}

public class UserVo {//省略其他屬性、get/set/construct方法
  private Long id;
  private String name;
  private String cellphone;
}

////////// Service+BO(Business Object) //////////
public class UserService {
  private UserRepository userRepository; //通過構造函數或者IOC框架注入
  
  public UserBo getUserById(Long userId) {
    UserEntity userEntity = userRepository.getUserById(userId);
    UserBo userBo = [...convert userEntity to userBo...];
    return userBo;
  }
}

public class UserBo {//省略其他屬性、get/set/construct方法
  private Long id;
  private String name;
  private String cellphone;
}

////////// Repository+Entity //////////
public class UserRepository {
  public UserEntity getUserById(Long userId) { //... }
}

public class UserEntity {//省略其他屬性、get/set/construct方法
  private Long id;
  private String name;
  private String cellphone;
}

平時開發 Web 後端項目的時候,基本上都是這麼組織代碼的。其中,UserEntityUserRepository 組成了數據訪問層,UserBoUserService 組成了業務邏輯層,UserVoUserController 在這裏屬於接口層。

從代碼中可以發現,UserBo 是一個純粹的數據結構,只包含數據,不包含任何業務邏輯。業務邏輯集中在 UserService 中。我們通過 UserService 來操作 UserBo。換句話說,Service 層的數據和業務邏輯,被分割為 BOService 兩個類中。像 UserBo 這樣,只包含數據,不包含業務邏輯的類,就叫作貧血模型Anemic Domain Model)。同理,UserEntityUserVo 都是基於貧血模型設計的。這種貧血模型將數據與操作分離,破壞了面向對象的封裝特性,是一種典型的面向過程的編程風格。

什麼是基於充血模型的 DDD 開發模式?

首先,我們先來看一下,什麼是充血模型?

在貧血模型中,數據和業務邏輯被分割到不同的類中。充血模型Rich Domain Model)正好相反,數據和對應的業務邏輯被封裝到同一個類中。因此,這種充血模型滿足面向對象的封裝特性,是典型的面向對象編程風格。

接下來,再來看一下,什麼是領域驅動設計?

領域驅動設計,即 DDD,主要是用來指導如何解耦業務系統,劃分業務模塊,定義業務領域模型及其交互。領域驅動設計這個概念並不新穎,早在 2004 年就被提出了,到現在已經有十幾年的歷史了。不過,它被大眾熟知,還是基於另一個概念的興起,那就是微服務。

除了監控、調用鏈追蹤、API 網關等服務治理系統的開發之外,微服務還有另外一個更加重要的工作,那就是針對公司的業務,合理地做微服務拆分。而領域驅動設計恰好就是用來指導劃分服務的。所以,微服務加速了領域驅動設計的盛行。

領域驅動設計有點兒類似敏捷開發、SOAPAAS 等概念,聽起來很高大上,但實際上只值“五分錢”。即便你沒有聽說過領域驅動設計,對這個概念一無所知,只要你是在開發業務系統,也或多或少都在使用它。做好領域驅動設計的關鍵是,看你對自己所做業務的熟悉程度,而並不是對領域驅動設計這個概念本身的掌握程度。即便你對領域驅動搞得再清楚,但是對業務不熟悉,也並不一定能做出合理的領域設計。所以,不要把領域驅動設計當銀彈,不要花太多的時間去過度地研究它。

實際上,基於充血模型的 DDD 開發模式實現的代碼,也是按照 MVC 三層架構分層的。Controller 層還是負責暴露接口,Repository 層還是負責數據存取,Service 層負責核心業務邏輯。它跟基於貧血模型的傳統開發模式的區別主要在 Service 層。

在基於貧血模型的傳統開發模式中,Service 層包含 Service 類和 BO 類兩部分,BO 是貧血模型,只包含數據,不包含具體的業務邏輯。業務邏輯集中在 Service 類中。在基於充血模型的 DDD 開發模式中,Service 層包含 Service 類和 Domain 類兩部分。Domain 就相當於貧血模型中的 BO。不過,DomainBO 的區別在於它是基於充血模型開發的,既包含數據,也包含業務邏輯。而 Service 類變得非常單薄。總結一下的話就是,基於貧血模型的傳統的開發模式,重 ServiceBO;基於充血模型的 DDD 開發模式,輕 ServiceDomain

為什麼基於貧血模型的傳統開發模式如此受歡迎?

基於貧血模型的傳統開發模式,將數據與業務邏輯分離,違反了 OOP 的封裝特性,實際上是一種面向過程的編程風格。但是,現在幾乎所有的 Web 項目,都是基於這種貧血模型的開發模式,甚至連 Java Spring 框架的官方 demo,都是按照這種開發模式來編寫的。

面向過程編程風格有種種弊端,比如,數據和操作分離之後,數據本身的操作就不受限制了。任何代碼都可以隨意修改數據。既然基於貧血模型的這種傳統開發模式是面向過程編程風格的,那它又為什麼會被廣大程序員所接受呢?

第一點原因是,大部分情況下,我們開發的系統業務可能都比較簡單,簡單到就是基於 SQLCRUD 操作,所以,我們根本不需要動腦子精心設計充血模型,貧血模型就足以應付這種簡單業務的開發工作。除此之外,因為業務比較簡單,即便我們使用充血模型,那模型本身包含的業務邏輯也並不會很多,設計出來的領域模型也會比較單薄,跟貧血模型差不多,沒有太大意義。

第二點原因是,充血模型的設計要比貧血模型更加有難度。因為充血模型是一種面向對象的編程風格。我們從一開始就要設計好針對數據要暴露哪些操作,定義哪些業務邏輯。而不是像貧血模型那樣,我們只需要定義數據,之後有什麼功能開發需求,我們就在 Service 層定義什麼操作,不需要事先做太多設計。

第三點原因是,思維已固化,轉型有成本。基於貧血模型的傳統開發模式經歷了這麼多年,已經深得人心、習以為常。你隨便問一個旁邊的大齡同事,基本上他過往參与的所有 Web 項目應該都是基於這個開發模式的,而且也沒有出過啥大問題。如果轉向用充血模型、領域驅動設計,那勢必有一定的學習成本、轉型成本。很多人在沒有遇到開發痛點的情況下,是不願意做這件事情的。

什麼項目應該考慮使用基於充血模型的 DDD 開發模式?

基於貧血模型的傳統的開發模式,比較適合業務比較簡單的系統開發。相對應的,基於充血模型的 DDD 開發模式,更適合業務複雜的系統開發。比如,包含各種利息計算模型、還款模型等複雜業務的金融系統。

這兩種開發模式,落實到代碼層面,區別不就是一個將業務邏輯放到 Service 類中,一個將業務邏輯放到 Domain 領域模型中嗎?為什麼基於貧血模型的傳統開發模式,就不能應對複雜業務系統的開發?而基於充血模型的 DDD 開發模式就可以呢?

實際上,除了我們能看到的代碼層面的區別之外(一個業務邏輯放到 Service 層,一個放到領域模型中),還有一個非常重要的區別,那就是兩種不同的開發模式會導致不同的開發流程。基於充血模型的 DDD 開發模式的開發流程,在應對複雜業務系統的開發的時候更加有優勢。為什麼這麼說呢?先來回憶一下,我們平時基於貧血模型的傳統的開發模式,都是怎麼實現一個功能需求的。

不誇張地講,我們平時的開發,大部分都是 SQL 驅動(SQL-Driven)的開發模式。我們接到一個後端接口的開發需求的時候,就去看接口需要的數據對應到數據庫中,需要哪張表或者哪幾張表,然後思考如何編寫 SQL 語句來獲取數據。之後就是定義 EntityBOVO,然後模板式地往對應的 RepositoryServiceController 類中添加代碼。

業務邏輯包裹在一個大的 SQL 語句中,而 Service 層可以做的事情很少。SQL 都是針對特定的業務功能編寫的,復用性差。當我要開發另一個業務功能的時候,只能重新寫個滿足新需求的 SQL 語句,這就可能導致各種長得差不多、區別很小的 SQL 語句滿天飛。

所以,在這個過程中,很少有人會應用領域模型、OOP 的概念,也很少有代碼復用意識。對於簡單業務系統來說,這種開發方式問題不大。但對於複雜業務系統的開發來說,這樣的開發方式會讓代碼越來越混亂,最終導致無法維護。

如果我們在項目中,應用基於充血模型的 DDD 的開發模式,那對應的開發流程就完全不一樣了。在這種開發模式下,我們需要事先理清楚所有的業務,定義領域模型所包含的屬性和方法。領域模型相當於可復用的業務中間層。新功能需求的開發,都基於之前定義好的這些領域模型來完成。

越複雜的系統,對代碼的復用性、易維護性要求就越高,我們就越應該花更多的時間和精力在前期設計上。而基於充血模型的 DDD 開發模式,正好需要我們前期做大量的業務調研、領域模型設計,所以它更加適合這種複雜系統的開發。

重點回顧

平時做 Web 項目的業務開發,大部分都是基於貧血模型的 MVC 三層架構,這裏把它稱為傳統的開發模式。之所以稱之為“傳統”,是相對於新興的基於充血模型的 DDD 開發模式來說的。基於貧血模型的傳統開發模式,是典型的面向過程的編程風格。相反,基於充血模型的 DDD 開發模式,是典型的面向對象的編程風格。

不過,DDD 也並非銀彈。對於業務不複雜的系統開發來說,基於貧血模型的傳統開發模式簡單夠用,基於充血模型的 DDD 開發模式有點大材小用,無法發揮作用。相反,對於業務複雜的系統開發來說,基於充血模型的 DDD 開發模式,因為前期需要在設計上投入更多時間和精力,來提高代碼的復用性和可維護性,所以相比基於貧血模型的開發模式,更加有優勢。

思考

  • 對於舉的例子中,UserEntityUserBoUserVo 包含的字段都差不多,是否可以合併為一個類呢?

參考:

本文由博客一文多發平台 發布!
更多內容請點擊我的博客

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

USB CONNECTOR 掌控什麼技術要點? 帶您認識其相關發展及效能

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

RocketMQ一個新的消費組初次啟動時從何處開始消費呢?

目錄

@(本文目錄)

1、拋出問題

一個新的消費組訂閱一個已存在的Topic主題時,消費組是從該Topic的哪條消息開始消費呢?

首先翻閱DefaultMQPushConsumer的API時,setConsumeFromWhere(ConsumeFromWhere consumeFromWhere)API映入眼帘,從字面意思來看是設置消費者從哪裡開始消費,正是解開該問題的”鑰匙“。ConsumeFromWhere枚舉類圖如下:

  • CONSUME_FROM_MAX_OFFSET
    從消費隊列最大的偏移量開始消費。
  • CONSUME_FROM_FIRST_OFFSET
    從消費隊列最小偏移量開始消費。
  • CONSUME_FROM_TIMESTAMP
    從指定的時間戳開始消費,默認為消費者啟動之前的30分鐘處開始消費。可以通過DefaultMQPushConsumer#setConsumeTimestamp。

是不是點小激動,還不快試試。

需求:新的消費組啟動時,從隊列最後開始消費,即只消費啟動后發送到消息服務器后的最新消息。

1.1 環境準備

本示例所用到的Topic路由信息如下:

Broker的配置如下(broker.conf)

brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH

storePathRootDir=E:/SH2019/tmp/rocketmq_home/rocketmq4.5_simple/store
storePathCommitLog=E:/SH2019/tmp/rocketmq_home/rocketmq4.5_simple/store/commitlog
namesrvAddr=127.0.0.1:9876
autoCreateTopicEnable=false
mapedFileSizeCommitLog=10240
mapedFileSizeConsumeQueue=2000

其中重點修改了如下兩個參數:

  • mapedFileSizeCommitLog
    單個commitlog文件的大小,這裏使用10M,方便測試用。
  • mapedFileSizeConsumeQueue
    單個consumequeue隊列長度,這裏使用1000,表示一個consumequeue文件中包含1000個條目。

1.2 消息發送者代碼

public static void main(String[] args) throws MQClientException, InterruptedException {
    DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
    producer.setNamesrvAddr("127.0.0.1:9876");
    producer.start();
    for (int i = 0; i < 300; i++) {
        try {
            Message msg = new Message("TopicTest" ,"TagA" , ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
            SendResult sendResult = producer.send(msg);
            System.out.printf("%s%n", sendResult);
        } catch (Exception e) {
            e.printStackTrace();
            Thread.sleep(1000);
        }
    }
    producer.shutdown();
}

通過上述,往TopicTest發送300條消息,發送完畢后,RocketMQ Broker存儲結構如下:

1.3 消費端驗證代碼

public static void main(String[] args) throws InterruptedException, MQClientException {
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("my_consumer_01");
    consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
    consumer.subscribe("TopicTest", "*");
    consumer.setNamesrvAddr("127.0.0.1:9876");
    consumer.registerMessageListener(new MessageListenerConcurrently() {
        @Override
        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
            ConsumeConcurrentlyContext context) {
            System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    });
    consumer.start();
    System.out.printf("Consumer Started.%n");
}

執行上述代碼后,按照期望,應該是不會消費任何消息,只有等生產者再發送消息后,才會對消息進行消費,事實是這樣嗎?執行效果如圖所示:

令人意外的是,竟然從隊列的最小偏移量開始消費了,這就“尷尬”了。難不成是RocketMQ的Bug。帶着這個疑問,從源碼的角度嘗試來解讀該問題,並指導我們實踐。

2、探究CONSUME_FROM_MAX_OFFSET實現原理

對於一個新的消費組,無論是集群模式還是廣播模式都不會存儲該消費組的消費進度,可以理解為-1,此時就需要根據DefaultMQPushConsumer#consumeFromWhere屬性來決定其從何處開始消費,首先我們需要找到其對應的處理入口。我們知道,消息消費者從Broker服務器拉取消息時,需要進行消費隊列的負載,即RebalanceImpl。

溫馨提示:本文不會詳細介紹RocketMQ消息隊列負載、消息拉取、消息消費邏輯,只會展示出通往該問題的簡短流程,如想詳細了解消息消費具體細節,建議購買筆者出版的《RocketMQ技術內幕》書籍。

RebalancePushImpl#computePullFromWhere

public long computePullFromWhere(MessageQueue mq) {
        long result = -1;                                                                                                                                                                                                                  // @1
        final ConsumeFromWhere consumeFromWhere = this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer().getConsumeFromWhere();    
        final OffsetStore offsetStore = this.defaultMQPushConsumerImpl.getOffsetStore();
        switch (consumeFromWhere) {
            case CONSUME_FROM_LAST_OFFSET_AND_FROM_MIN_WHEN_BOOT_FIRST:
            case CONSUME_FROM_MIN_OFFSET:
            case CONSUME_FROM_MAX_OFFSET:
            case CONSUME_FROM_LAST_OFFSET: {                                                                                                                                                                // @2
               // 省略部分代碼
                break;
            }
            case CONSUME_FROM_FIRST_OFFSET: {                                                                                                                                                              // @3
                // 省略部分代碼
                break;
            }
            case CONSUME_FROM_TIMESTAMP: {                                                                                                                                                                  //@4
                // 省略部分代碼
                break;
            }
            default:
                break;
        }
        return result;                                                                                                                                                                                                                  // @5
    }

代碼@1:先解釋幾個局部變量。

  • result
    最終的返回結果,默認為-1。
  • consumeFromWhere
    消息消費者開始消費的策略,即CONSUME_FROM_LAST_OFFSET等。
  • offsetStore
    offset存儲器,消費組消息偏移量存儲實現器。

代碼@2:CONSUME_FROM_LAST_OFFSET(從隊列的最大偏移量開始消費)的處理邏輯,下文會詳細介紹。

代碼@3:CONSUME_FROM_FIRST_OFFSET(從隊列最小偏移量開始消費)的處理邏輯,下文會詳細介紹。

代碼@4:CONSUME_FROM_TIMESTAMP(從指定時間戳開始消費)的處理邏輯,下文會詳細介紹。

代碼@5:返回最後計算的偏移量,從該偏移量出開始消費。

2.1 CONSUME_FROM_LAST_OFFSET計算邏輯

case CONSUME_FROM_LAST_OFFSET: {
    long lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_STORE);   // @1
    if (lastOffset >= 0) {                                                                                                             // @2
        result = lastOffset;
    }
    // First start,no offset
    else if (-1 == lastOffset) {                                                                                                  // @3
        if (mq.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {               
            result = 0L;
        } else {
            try {
                result = this.mQClientFactory.getMQAdminImpl().maxOffset(mq);                     
            } catch (MQClientException e) {                                                                              // @4
                result = -1;
            }
        }
    } else {
        result = -1;    
    }
    break;
}

代碼@1:使用offsetStore從消息消費進度文件中讀取消費消費進度,本文將以集群模式為例展開。稍後詳細分析。

代碼@2:如果返回的偏移量大於等於0,則直接使用該offset,這個也能理解,大於等於0,表示查詢到有效的消息消費進度,從該有效進度開始消費,但我們要特別留意lastOffset為0是什麼場景,因為返回0,並不會執行CONSUME_FROM_LAST_OFFSET(語義)。

代碼@3:如果lastOffset為-1,表示當前並未存儲其有效偏移量,可以理解為第一次消費,如果是消費組重試主題,從重試隊列偏移量為0開始消費;如果是普通主題,則從隊列當前的最大的有效偏移量開始消費,即CONSUME_FROM_LAST_OFFSET語義的實現。

代碼@4:如果從遠程服務拉取最大偏移量拉取異常或其他情況,則使用-1作為第一次拉取偏移量。

分析,上述執行的現象,雖然設置的是CONSUME_FROM_LAST_OFFSET,但現象是從隊列的第一條消息開始消費,根據上述源碼的分析,只有從消費組消費進度存儲文件中取到的消息偏移量為0時,才會從第一條消息開始消費,故接下來重點分析消息消費進度存儲器(OffsetStore)在什麼情況下會返回0。

接下來我們將以集群模式來查看一下消息消費進度的查詢邏輯,集群模式的消息進度存儲管理器實現為:
RemoteBrokerOffsetStore,最終Broker端的命令處理類為:ConsumerManageProcessor。

ConsumerManageProcessor#queryConsumerOffset
private RemotingCommand queryConsumerOffset(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException {
    final RemotingCommand response =
        RemotingCommand.createResponseCommand(QueryConsumerOffsetResponseHeader.class);
    final QueryConsumerOffsetResponseHeader responseHeader =
        (QueryConsumerOffsetResponseHeader) response.readCustomHeader();
    final QueryConsumerOffsetRequestHeader requestHeader =
        (QueryConsumerOffsetRequestHeader) request
            .decodeCommandCustomHeader(QueryConsumerOffsetRequestHeader.class);

    long offset =
        this.brokerController.getConsumerOffsetManager().queryOffset(
            requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId());    // @1

    if (offset >= 0) {                                                                                                                                          // @2
        responseHeader.setOffset(offset);
        response.setCode(ResponseCode.SUCCESS);
        response.setRemark(null);
    } else {                                                                                                                                                       // @3
        long minOffset =
            this.brokerController.getMessageStore().getMinOffsetInQueue(requestHeader.getTopic(),
                requestHeader.getQueueId());                                                                                                     // @4
        if (minOffset <= 0
            && !this.brokerController.getMessageStore().checkInDiskByConsumeOffset(                                // @5
            requestHeader.getTopic(), requestHeader.getQueueId(), 0)) {
            responseHeader.setOffset(0L);
            response.setCode(ResponseCode.SUCCESS);
            response.setRemark(null);
        } else {                                                                                                                                                 // @6
            response.setCode(ResponseCode.QUERY_NOT_FOUND);
            response.setRemark("Not found, V3_0_6_SNAPSHOT maybe this group consumer boot first");
        }
    }
    return response;
}

代碼@1:從消費消息進度文件中查詢消息消費進度。

代碼@2:如果消息消費進度文件中存儲該隊列的消息進度,其返回的offset必然會大於等於0,則直接返回該偏移量該客戶端,客戶端從該偏移量開始消費。

代碼@3:如果未從消息消費進度文件中查詢到其進度,offset為-1。則首先獲取該主題、消息隊列當前在Broker服務器中的最小偏移量(@4)。如果小於等於0(返回0則表示該隊列的文件還未曾刪除過)並且其最小偏移量對應的消息存儲在內存中而不是存在磁盤中,則返回偏移量0,這就意味着ConsumeFromWhere中定義的三種枚舉類型都不會生效,直接從0開始消費,到這裏就能解開其謎團了(@5)。

代碼@6:如果偏移量小於等於0,但其消息已經存儲在磁盤中,此時返回未找到,最終RebalancePushImpl#computePullFromWhere中得到的偏移量為-1。

看到這裏,大家應該能回答文章開頭處提到的問題了吧?

看到這裏,大家應該明白了,為什麼設置的CONSUME_FROM_LAST_OFFSET,但消費組是從消息隊列的開始處消費了吧,原因就是消息消費進度文件中並沒有找到其消息消費進度,並且該隊列在Broker端的最小偏移量為0,說的更直白點,consumequeue/topicName/queueNum的第一個消息消費隊列文件為00000000000000000000,並且消息其對應的消息緩存在Broker端的內存中(pageCache),其返回給消費端的偏移量為0,故會從0開始消費,而不是從隊列的最大偏移量處開始消費。

為了知識體系的完備性,我們順便來看一下其他兩種策略的計算邏輯。

2.2 CONSUME_FROM_FIRST_OFFSET

case CONSUME_FROM_FIRST_OFFSET: {
    long lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_STORE);   // @1
    if (lastOffset >= 0) {    // @2
        result = lastOffset;
    } else if (-1 == lastOffset) {  // @3
        result = 0L;
    } else {                                  
        result = -1;                    // @4
    }
    break;
}

從隊列的開始偏移量開始消費,其計算邏輯如下:
代碼@1:首先通過偏移量存儲器查詢消費隊列的消費進度。

代碼@2:如果大於等於0,則從當前該偏移量開始消費。

代碼@3:如果遠程返回-1,表示並沒有存儲該隊列的消息消費進度,從0開始。

代碼@4:否則從-1開始消費。

2.4 CONSUME_FROM_TIMESTAMP

從指定時戳后的消息開始消費。

case CONSUME_FROM_TIMESTAMP: {
    ong lastOffset = offsetStore.readOffset(mq, ReadOffsetType.READ_FROM_STORE);   // @1
    if (lastOffset >= 0) {                                                                                                            // @2
        result = lastOffset;
    } else if (-1 == lastOffset) {                                                                                                 // @3
        if (mq.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
            try {
                result = this.mQClientFactory.getMQAdminImpl().maxOffset(mq);
            } catch (MQClientException e) {
                result = -1;
            }
        } else {
            try {
                long timestamp = UtilAll.parseDate(this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer().getConsumeTimestamp(),
                    UtilAll.YYYYMMDDHHMMSS).getTime();
                result = this.mQClientFactory.getMQAdminImpl().searchOffset(mq, timestamp);
            } catch (MQClientException e) {
                result = -1;
            }
        }
    } else {
        result = -1;
    }
    break;
}

其基本套路與CONSUME_FROM_LAST_OFFSET一樣:
代碼@1:首先通過偏移量存儲器查詢消費隊列的消費進度。

代碼@2:如果大於等於0,則從當前該偏移量開始消費。

代碼@3:如果遠程返回-1,表示並沒有存儲該隊列的消息消費進度,如果是重試主題,則從當前隊列的最大偏移量開始消費,如果是普通主題,則根據時間戳去Broker端查詢,根據查詢到的偏移量開始消費。

原理就介紹到這裏,下面根據上述理論對其進行驗證。

3、猜想與驗證

根據上述理論分析我們得知設置CONSUME_FROM_LAST_OFFSET但並不是從消息隊列的最大偏移量開始消費的“罪魁禍首”是因為消息消費隊列的最小偏移量為0,如果不為0,則就會符合預期,我們來驗證一下這個猜想。
首先我們刪除commitlog目錄下的文件,如圖所示:

其消費隊列截圖如下:

消費端的驗證代碼如下:

public static void main(String[] args) throws InterruptedException, MQClientException {
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("my_consumer_02");
    consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
    consumer.subscribe("TopicTest", "*");
    consumer.setNamesrvAddr("127.0.0.1:9876");
    consumer.registerMessageListener(new MessageListenerConcurrently() {
        @Override
        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
            ConsumeConcurrentlyContext context) {
            System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    });
    consumer.start();
    System.out.printf("Consumer Started.%n");
}

運行結果如下:

並沒有消息存在的消息,符合預期。

4、解決方案

如果在生產環境下,一個新的消費組訂閱一個已經存在比較久的topic,設置CONSUME_FROM_MAX_OFFSET是符合預期的,即該主題的consumequeue/{queueNum}/fileName,fileName通常不會是00000000000000000000,如是是上面文件名,想要實現從隊列的最後開始消費,該如何做呢?那就走自動創建消費組的路子,執行如下命令:

./mqadmin updateSubGroup -n 127.0.0.1:9876 -c DefaultCluster -g my_consumer_05

//克隆一個訂閱了該topic的消費組消費進度
./mqadmin cloneGroupOffset -n 127.0.0.1:9876 -s my_consumer_01 -d my_consumer_05 -t TopicTest

//重置消費進度到當前隊列的最大值
./mqadmin resetOffsetByTime -n 127.0.0.1:9876 -g my_consumer_05 -t TopicTest -s -1

按照上上述命令后,即可實現其目的。

您都看到這裏了,麻煩幫忙點個贊,謝謝您的認可與鼓勵。

作者介紹:
丁威,《RocketMQ技術內幕》作者,RocketMQ 社區佈道師,公眾號: 維護者,目前已陸續發表源碼分析Java集合、Java 併發包(JUC)、Netty、Mycat、Dubbo、RocketMQ、Mybatis等源碼專欄。歡迎加入我的知識星球,構建一個高質量的技術交流社群。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※公開收購3c價格,不怕被賤賣!

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象