標籤: 網頁設計公司

  • Spring Security 實戰乾貨:如何實現不同的接口不同的安全策略

    Spring Security 實戰乾貨:如何實現不同的接口不同的安全策略

    1. 前言

    歡迎閱讀 Spring Security 實戰乾貨 系列文章 。最近有開發小夥伴提了一個有趣的問題。他正在做一個項目,涉及兩種風格,一種是給小程序出接口,安全上使用無狀態的JWT Token;另一種是管理後台使用的是Freemarker,也就是前後端不分離的Session機制。用Spring Security該怎麼辦?

    2. 解決方案

    我們可以通過多次繼承WebSecurityConfigurerAdapter構建多個HttpSecurityHttpSecurity 對象會告訴我們如何驗證用戶的身份,如何進行訪問控制,採取的何種策略等等。

    如果你看過之前的教程,我們是這麼配置的:

    /**
     * 單策略配置
     *
     * @author felord.cn
     * @see org.springframework.boot.autoconfigure.security.servlet.SpringBootWebSecurityConfiguration
     * @since 14 :58 2019/10/15
     */
    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true, securedEnabled = true)
    @EnableWebSecurity
    @ConditionalOnClass(WebSecurityConfigurerAdapter.class)
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    public class CustomSpringBootWebSecurityConfiguration {
        
        /**
         * The type Default configurer adapter.
         */
        @Configuration
        @Order(SecurityProperties.BASIC_AUTH_ORDER)
        static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
    
            @Override
            protected void configure(AuthenticationManagerBuilder auth) throws Exception {
                super.configure(auth);
            }
    
            @Override
            public void configure(WebSecurity web) {
                super.configure(web);
            }
    
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                // 配置 httpSecurity
    
            }
        }
    }
    

    上面的配置了一個HttpSecurity,我們如法炮製再增加一個WebSecurityConfigurerAdapter的子類來配置另一個HttpSecurity。伴隨而來的還有不少的問題要解決。

    2.1 如何路由不同的安全配置

    我們配置了兩個HttpSecurity之後,程序如何讓小程序接口和後台接口走對應的HttpSecurity

    HttpSecurity.antMatcher(String antPattern)可以提供過濾機制。比如我們配置:

         @Override
            protected void configure(HttpSecurity http) throws Exception {
                // 配置 httpSecurity
                http.antMatcher("/admin/v1");
    
            }
    

    那麼該HttpSecurity將只提供給以/admin/v1開頭的所有URL。這要求我們針對不同的客戶端指定統一的URL前綴。

    舉一反三隻要HttpSecurity提供的功能都可以進行個性化定製。比如登錄方式,角色體系等。

    2.2 如何指定默認的HttpSecurity

    我們可以通過在WebSecurityConfigurerAdapter實現上使用@Order註解來指定優先級,數值越大優先級越低,沒有@Order註解將優先級最低。

    2.3 如何配置不同的UserDetailsService

    很多情況下我們希望普通用戶和管理用戶完全隔離,我們就需要多個UserDetailsService,你可以在下面的方法中對AuthenticationManagerBuilder進行具體的設置來配置UserDetailsService,同時也可以配置不同的密碼策略。

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                // 自行實現
                return  null ;
            }
        });
        // 也可以設計特定的密碼策略
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder);
        auth.authenticationProvider(daoAuthenticationProvider);
    }
    

    2.4 最終的配置模板

    上面的幾個問題解決之後,我們基本上掌握了在一個應用中執行多種安全策略。配置模板如下:

    /**
     * 多個策略配置
     *
     * @author felord.cn
     * @see org.springframework.boot.autoconfigure.security.servlet.SpringBootWebSecurityConfiguration
     * @since 14 :58 2019/10/15
     */
    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true, securedEnabled = true)
    @EnableWebSecurity
    @ConditionalOnClass(WebSecurityConfigurerAdapter.class)
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    public class CustomSpringBootWebSecurityConfiguration {
    
        /**
         * 後台接口安全策略. 默認配置
         */
        @Configuration
        @Order(1)
        static class AdminConfigurerAdapter extends WebSecurityConfigurerAdapter {
    
            @Override
            protected void configure(AuthenticationManagerBuilder auth) throws Exception {
                DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
                //用戶詳情服務個性化
                daoAuthenticationProvider.setUserDetailsService(new UserDetailsService() {
                    @Override
                    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                        // 自行實現
                        return null;
                    }
                });
                // 也可以設計特定的密碼策略
                BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
                daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder);
                auth.authenticationProvider(daoAuthenticationProvider);
            }
    
            @Override
            public void configure(WebSecurity web) {
                super.configure(web);
            }
    
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                // 根據需求自行定製
                http.antMatcher("/admin/v1")
                        .sessionManagement(Customizer.withDefaults())
                        .formLogin(Customizer.withDefaults());
    
    
            }
        }
    
        /**
         * app接口安全策略. 沒有{@link Order}註解優先級比上面低
         */
        @Configuration
        static class AppConfigurerAdapter extends WebSecurityConfigurerAdapter {
    
            @Override
            protected void configure(AuthenticationManagerBuilder auth) throws Exception {
                DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
                //用戶詳情服務個性化
                daoAuthenticationProvider.setUserDetailsService(new UserDetailsService() {
                    @Override
                    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                        // 自行實現
                        return null;
                    }
                });
                // 也可以設計特定的密碼策略
                BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
                daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder);
                auth.authenticationProvider(daoAuthenticationProvider);
            }
    
            @Override
            public void configure(WebSecurity web) {
                super.configure(web);
            }
    
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                // 根據需求自行定製
                http.antMatcher("/app/v1")
                        .sessionManagement(Customizer.withDefaults())
                        .formLogin(Customizer.withDefaults());
    
    
            }
        }
    }
    

    3. 總結

    今天我們解決了如何針對不同類型接口採取不同的安全策略的方法,希望對你有用,如果你有什麼問題可以留言。多多關注:碼農小胖哥,更多乾貨奉上。

    關注公眾號:Felordcn 獲取更多資訊

    個人博客:https://felord.cn

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

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

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

    ※教你寫出一流的銷售文案?

  • 【Java Spring Cloud 實戰之路】添加一個SpringBootAdmin監控

    【Java Spring Cloud 實戰之路】添加一個SpringBootAdmin監控

    0. 前言

    在之前的幾章中,我們先搭建了一個項目骨架,又搭建了一個使用nacos的gateway網關項目,網關項目中並沒有配置太多的東西。現在我們就接着搭建在Spring Cloud 微服務中另一個重要的項目 – Spring boot admin.

    1. Spring Boot Admin 介紹

    Spring Boot Admin 用來監控基於Spring Boot的應用,在Spring Boot Actuator的基礎上提供了簡潔的可視化Web UI。Spring Boot Admin 提供了以下功能:

    • 显示應用的健康狀態
    • 显示應用的細節內容: JVM和內存信息,micrometer信息, 數據源信息,緩存信息等
    • 显示 編譯版本
    • 查看和下載日誌
    • 查看jvm參數和環境變量值
    • 查看Spring Boot項目配置
    • 显示 thread dump
    • 显示 http-traces

    ……

    等一系列內容。

    2. 創建一個 Spring Boot Admin項目

    那麼,我們就來創建一個Spring Boot Admin 項目吧。

    2.1 創建 Spring Boot Admin 服務端

    在manager 目錄下,創建一個 monitor目錄,並在monitor目錄下創建一個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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>monitor</artifactId>
        <version>${revision}</version>
        <packaging>jar</packaging>
        <parent>
            <artifactId>manager</artifactId>
            <groupId>club.attachie</groupId>
            <version>${revision}</version>
        </parent>
    
    </project>
    

    在 manager/pom.xml 註冊我們新建的項目模塊:

    <modules>
        <module>gateway</module>
        <module>monitor</module>
    </modules>
    

    在 monitor 創建如下目錄:

    .
    ├── pom.xml
    └── src
        └── main
            ├── java
            └── resources
    

    在根目錄的pom.xml 添加 Spring Boot Admin 依賴:

    先添加spring-boot-admin版本號變量:

    <spring-boot-admin.version>2.2.3</spring-boot-admin.version>
    

    並在dependencyManagement > dependencies 下添加:

    <dependency>
        <groupId>de.codecentric</groupId>
        <artifactId>spring-boot-admin-starter-server</artifactId>
        <version>${spring-boot-admin.version}</version>
    </dependency>
    

    在monitor/pom.xml文件中添加:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-server</artifactId>
        </dependency>
    </dependencies>
    

    運行

    mvn clean install

    檢查並刷mvn引用緩存。

    創建MonitorApplication類:

    package club.attachie.nature.monitor;
    
    import de.codecentric.boot.admin.server.config.EnableAdminServer;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    @EnableAdminServer
    public class MonitorApplication {
        public static void main(String[] args) {
            SpringApplication.run(MonitorApplication.class, args);
        }
    }
    

    啟動后能看到如下界面:

    3 與網關服務進行互通

    在上一篇中,我們添加了Spring Cloud Gateway項目,到目前為止兩個項目之間完全割裂沒有關聯。在這一節,我們在兩者之間建立關聯。也就是說,將gateway 項目引入Spring Admin Boot監聽。

    在 manager/gateway 的pom.xml 文件中加入如下引用:

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

    然後修改 gateway項目的啟動端口,在resources/bootstrap.yml 添加:

    server:
      port: 8070
    

    在 monitor中加入nacos引用:

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>      
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    

    修改MonitorApplication 為:

    package club.attachie.nature.monitor;
    
    import de.codecentric.boot.admin.server.config.EnableAdminServer;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.context.config.annotation.RefreshScope;
    
    @SpringBootApplication
    @EnableAdminServer
    @RefreshScope
    public class MonitorApplication {
        public static void main(String[] args) {
            SpringApplication.run(MonitorApplication.class, args);
        }
    }
    

    創建monitor項目的bootsrap.yml:

    spring:
      application:
        name: monitor
      
      cloud:
      	nacos:
          discovery:
            server-addr: 127.0.0.1:8848
    

    關於這裏的配置 在上一篇 中有個錯誤,應該是 discovery > server-addr,不是 config > server-addr。兩者有區別,discovery表示設置nacos為服務發現中心,config表示nacos為配置中心。

    啟動 gateway 項目和 monitor項目查看效果, 訪問 8080端口:

    可以看到兩個應用可以被發現,如果沒有設置monitor項目把nacos當做服務發現中心,將無法獲取到具體在線的應用。點擊 gateway 進去后可以查看到:

    4. 總結

    我們搭建了一個Spring Boot Admin 項目作為一個監控系統,後續會在這裏添加更多的內容。

    更多內容煩請關注我的博客《高先生小屋》

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※台北網頁設計公司全省服務真心推薦

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※推薦評價好的iphone維修中心

  • 小菜成長之路,警惕淪為 API 調用俠

    小菜成長之路,警惕淪為 API 調用俠

    小菜(化名)在某互聯網公司擔任運維工程師,負責公司後台業務的運維保障工作。由於自己編程經驗不多,平時有不少工作需要開發協助。

    聽說 Python 很火,能快速開發一些運維腳本,小菜也加入 Python 大軍學起來。 Python 語言確實簡單,小菜很快就上手了,覺得自己應對運維開發工作已經綽綽有餘,便不再深入研究。

    背景

    這天老闆給小菜派了一個數據採集任務,要實時統計服務器 TCP 連接數。需求背景是這樣的:開發同事需要知道服務的連接數以及不同狀態連接的比例,以便判斷服務狀態。

    因此,小菜需要開發一個腳本,定期採集並報告 TCP 連接數,提交數據格式定為 json :

    {
      "LISTEN": 4,
      "ESTABLISHED": 100,
      "TIME_WAIT": 10
    }
    

    作為運維工程師,小菜當然知道怎麼查看系統 TCP 連接。
    Linux 系統中有兩個命令可以辦到, netstat 和 ss :

    $ netstat -nat
    Active Internet connections (servers and established)
    Proto Recv-Q Send-Q Local Address           Foreign Address         State
    tcp        0      0 127.0.0.1:8388          0.0.0.0:*               LISTEN
    tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN
    tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN
    tcp        0      0 192.168.56.3:22         192.168.56.1:54983      ESTABLISHED
    tcp6       0      0 :::22                   :::*                    LISTEN
    
    $ ss -nat
    State                    Recv-Q                    Send-Q                                         Local Address:Port                                         Peer Address:Port
    LISTEN                   0                         128                                                127.0.0.1:8388                                              0.0.0.0:*
    LISTEN                   0                         128                                            127.0.0.53%lo:53                                                0.0.0.0:*
    LISTEN                   0                         128                                                  0.0.0.0:22                                                0.0.0.0:*
    ESTAB                    0                         0                                               192.168.56.3:22                                           192.168.56.1:54983
    LISTEN                   0                         128                                                     [::]:22                                                   [::]:*
    

    小菜還知道 ss 命令比 netstat 命令要快,但至於為什麼,小菜就不知道了。

    小菜很快找到老闆,提出了自己的解決方案:寫一個 Python 程序,調用 ss 命令採集 TCP 連接信息,然後再逐條統計。

    老闆告訴小菜,線上服務器很多都是最小化安裝,並不能保證每台機器上都有 ss 或者 netstat 命令。

    老闆還告訴小菜,程序開發要學會 站在巨人的肩膀上 。動手寫代碼前,先調研一番,看是否有現成的解決方案。 切忌重複造輪子 ,浪費時間不說,可能代碼質量還差,效果也不好。

    最後老闆給小菜指了條明路,讓他回去再看看 psutil 。 psutil 是一個 Python 第三方包,用於採集系統性能數據,包括: CPU 、內存、磁盤、網卡以及進程等等。臨走前,老闆還叮囑小菜,完成工作后花點時間研究下這個庫。

    psutil 方案

    小菜搜索 psutil 發現,原來有這麼順手的第三方庫,喜出望外!他立馬裝好 psutil ,準備開干:

    $ pip install psutil
    

    導入 psutil 后,一個函數調用就可以拿到系統所有連接,連接信息非常豐富:

    >>> import psutil
    >>> for conn in psutil.net_connections('tcp'):
    ...     print(conn)
    ...
    sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='192.168.56.3', port=22), raddr=addr(ip='192.168.56.1', port=54983), status='ESTABLISHED', pid=None)
    sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='127.0.0.1', port=8388), raddr=(), status='LISTEN', pid=None)
    sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='0.0.0.0', port=22), raddr=(), status='LISTEN', pid=None)
    sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='127.0.0.53', port=53), raddr=(), status='LISTEN', pid=None)
    sconn(fd=-1, family=<AddressFamily.AF_INET6: 10>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='::', port=22), raddr=(), status='LISTEN', pid=None)
    

    小菜很滿意,感覺不用花多少時間就可搞定數據採集需求了,準時下班有望!噼里啪啦,很快小菜就寫下這段代碼:

    import psutil
    from collections import defaultdict
    
    # 遍歷每個連接,按連接狀態累加
    stats = defaultdict(int)
    for conn in psutil.net_connections('tcp'):
        stats[conn.status] += 1
    
    # 遍歷每種狀態,輸出連接數
    for status, count in stats.items():
        print(status, count)
    

    小菜接着在服務器上測試這段代碼,功能完全正常:

    ESTABLISHED 1
    LISTEN 4
    

    小菜將數據採集腳本提交,並按既定節奏逐步發布到生產服務器上。開發同事很快就看到小菜採集的數據,都誇小菜能力不錯,需求完成得很及時。小菜也很高興,感覺 Python 沒白學。如果用其他語言開發,說不定現在還在加班加點呢!Life is short, use Python! 果然沒錯!

    小菜愈發自信,早就把老闆的話拋到腦後了。 psutil 這個庫這麼好上手,有啥好深入研究的?

    內存悲劇

    突然有一天,其他同事緊急告訴小菜,他開發的採集腳本佔用很多內存, CPU 也跑到了 100% ,已經開始影響線上服務了。小菜還沉浸在成功的喜悅中,收到這個反饋如同晴天霹靂,有點举手無措。

    業務同事告訴小菜,受影響的機器系統連接數非常大,質疑小菜是不是腳本存在性能問題。小菜覺得很背,腳本只是調用 psutil 並統計數據,怎麼就攤上性能故障?腳本影響線上服務,小菜壓力很大,但不知道如何是好,只能跑去找老闆尋求幫助。

    老闆要小菜第一時間停止數據採集,降低影響。復盤故障時,老闆很敏銳地問小菜,是不是用容器保存所有連接了?小菜自己並沒有,但是 psutil 這麼做了:

    >>> psutil.net_connections()
    [sconn(fd=-1, family=<AddressFamily.AF_INET6: 10>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='::', port=22), raddr=(), status='LISTEN', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='0.0.0.0', port=22), raddr=(), status='LISTEN', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='127.0.0.53', port=53), raddr=(), status='LISTEN', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_DGRAM: 2>, laddr=addr(ip='10.0.2.15', port=68), raddr=(), status='NONE', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_DGRAM: 2>, laddr=addr(ip='127.0.0.1', port=8388), raddr=(), status='NONE', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='192.168.56.3', port=22), raddr=addr(ip='192.168.56.1', port=54983), status='ESTABLISHED', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_DGRAM: 2>, laddr=addr(ip='127.0.0.53', port=53), raddr=(), status='NONE', pid=None), sconn(fd=-1, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='127.0.0.1', port=8388), raddr=(), status='LISTEN', pid=None)]
    

    psutil 將採集到的所有 TCP 連接放在一個列表裡返回。如果服務器上有十萬個 TCP 連接,那麼列表裡將有十萬個連接對象。難怪採集腳本吃了那麼多內存!

    老闆告訴小菜,可以用生成器加以解決。與列表不同,生成器逐個返回數據,因此不會佔用太多內存。Python2 中 range 和 xrange 函數的區別也是一樣的道理。

    小菜從 pstuil  fork 了一個分支,並將 net_connections 函數改造成 生成器 :

    def net_connections():
        while True:
            if done:
                break
    
            # 解析一個TCP連接
            conn = xxx
    
            yield conn
    

    代碼上線后,採集腳本內存佔用量果然下降了! 生成器 將統計算法的空間複雜度由原來的 O(n) 優化為 O(1) 。經過這次教訓,小菜不敢再盲目自信了,他決定抽時間好好看看 psutil 的源碼。

    源碼體會

    深入學習源碼后,小菜發現原來 psutil 採集 TCP 連接數的秘笈是:從 /proc/net/tcp 以及 /proc/net/tcp6 讀取連接信息。

    由此,他還進一步了解到 procfs ,這是一個偽文件系統,將內核空間信息以文件方式暴露到用戶空間。 /proc/net/tcp 文件則是提供內核 TCP 連接信息:

    $ cat /proc/net/tcp
      sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
       0: 0100007F:20C4 00000000:0000 0A 00000000:00000000 00:00000000 00000000 65534        0 18183 1 0000000000000000 100 0 0 10 0
       1: 3500007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000   101        0 16624 1 0000000000000000 100 0 0 10 0
       2: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 18967 1 0000000000000000 100 0 0 10 0
       3: 0338A8C0:0016 0138A8C0:D6C7 01 00000000:00000000 02:00023B11 00000000     0        0 22284 4 0000000000000000 20 13 23 10 20
    

    小菜還注意到,連接信息看起來像個自定義類對象,但其實是一個 nametuple :

    # psutil.net_connections()
    sconn = namedtuple('sconn', ['fd', 'family', 'type', 'laddr', 'raddr',
                                 'status', 'pid'])
    

    小菜一開始並不知道作者為啥要這麼做。後來,小菜開始研究 Python 源碼,學習了 Python 類機制后他恍然大悟。

    Python 自定義類的每個實例對象均需要一個 dict 來保存對象屬性,這也就是對象的 屬性空間 。

    如果用自定義類來實現,每個連接都需要創建一個字典,而字典又是 散列表 實現的。如果系統存在成千上萬的連接,開銷可想而知。

    小菜將學到的知識總結起來:對於 數量大 而 屬性固定 的實體,沒有必要用自定義類來實現,用 nametuple 更合適,開銷更小。由此,小菜不經由衷佩服 psutil 的作者。

    CPU悲劇

    後來小菜又收到業務反饋,採集腳本在高併發的服務器上, CPU 使用率很高,需要再優化一下。

    小菜回憶 psutil 源碼,很快就找到了性能瓶頸處: psutil 將連接信息所有字段都解析了,而採集腳本只需要其中的 狀態 字段而已。

    跟老闆商量后,小菜決定自行讀取 procfs 來實現採集腳本,只解析狀態字段,避免不必要的計算開銷。

    procfs 方案

    直接讀取 /proc/net/tcp ,可以得到完整的 TCP 連接信息:

    >>> with open('/proc/net/tcp') as f:
    ...     for line in f:
    ...         print(line.rstrip())
    ...
      sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
       0: 0100007F:20C4 00000000:0000 0A 00000000:00000000 00:00000000 00000000 65534        0 18183 1 0000000000000000 100 0 0 10 0
       1: 3500007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000   101        0 16624 1 0000000000000000 100 0 0 10 0
       2: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 18967 1 0000000000000000 100 0 0 10 0
       3: 0338A8C0:0016 0138A8C0:D6C7 01 00000000:00000000 02:0007169E 00000000     0        0 22284 3 0000000000000000 20 20 33 10 20
    

    其中, IP 、端口、狀態等字段都是以十六進制編碼的。例如, st 列表示狀態,狀態碼 0A 表示 LISTEN 。很快小菜就寫下這段代碼:

    from collections import defaultdict
    
    stat_names = {
        '0A': 'LISTEN',
        '01': 'ESTABLISHED',
        # ...
    }
    
    # 遍歷每個連接,按連接狀態累加
    stats = defaultdict(int)
    
    with open('/proc/net/tcp') as f:
        # 跳過表頭行
        f.readline()
    
        for line in f:
            st = line.strip().split()[3]
            stats[st] += 1
    
    for st, count in stats.items():
        print(stat_names[st], count)
    

    現在,小菜寫代碼比之前講究多了。在統計連接數時,他並不急於將狀態碼解析成名字,而是按原樣統計。等統計完成,他再一次性轉換,這樣狀態碼轉換開銷便降到最低: O(1)  而不是 O(n) 。

    這次改進符合業務同事預期,但小菜決定好好做一遍性能測試,不打無準備之仗。他找業務同事要了一個連接數最大的 /proc/net/tcp 樣本,拉到本地測試。測試結果還算符合預期,採集腳本能夠扛住十萬連接採集壓力。

    性能測試中,小菜發現了一個比較奇怪的問題。同樣的連接規模,把 /proc/net/tcp 拉到本地跑比直接在服務器上跑要快,而本地電腦性能肯定比不上服務器。

    他百思不得其解,又去找老闆幫忙。老闆很快指出到其中的區別,將 /proc/net/tcp 拉到本地就成為普通 磁盤文件 ,而 procfs 是內核映射出來的 偽文件 ,並不是磁盤文件。

    他讓小菜研究一下 Python 文件 IO 以及內核 IO 子系統在處理這兩種文件時有什麼區別,還讓小菜特別留意 IO 緩衝區大小。

    IO緩衝

    小菜打開一個普通的磁盤文件,發現 Python 選的默認緩衝區大小是 4K (讀緩存對象頭 152 字節):

    >>> f = open('test.py')
    >>> f.buffer.__sizeof__()
    4248
    

    但是如果打開的是 procfs 文件, Python 選的緩衝區卻只有 1K ,相差了 4 倍呢!

    >>> f = open('/proc/net/tcp')
    >>> f.buffer.__sizeof__()
    1176
    

    因此,理論上 Python 默認讀取 procfs 發生的上下文切換次數是普通磁盤文件的 4 倍,怪不得會慢。

    雖然小菜還不知道這種現象背後的原因,但是他已經知道怎麼進行優化了。隨即他決定將緩衝區設置為 1M 以上,盡量避免 IO 上下文切換,以空間換時間:

    with open('/proc/net/tcp', buffering=1*1024*1024) as f:
        # ...
    

    經過這次優化,採集腳本在大部分服務器上運行良好,基本可以高枕無憂了。而小菜也意識到 編程語言 以及 操作系統 等底層基礎知識的重要性,他開始制定學習計劃補全計算機基礎知識。

    netlink 方案

    後來負載均衡團隊找到小菜,他們也想統計服務器上的連接信息。由於負載均衡服務器作為入口轉發流量,連接數規模特別大,達到幾十萬,將近百萬的規模。小菜決定好好進行性能測試,再視情況上線。

    測試結果並不樂觀,採集腳本要跑幾十秒鐘才完成, CPU 跑到 100% 。小菜再次調高 IO 緩衝區,但效果不明顯。小菜又測試了 ss 命令,發現 ss 命令要快很多。由於之前嘗到了閱讀源碼的甜頭,小菜很想到 ss 源碼中尋找秘密。

    由於項目時間較緊,老闆提醒小菜先用 strace 命令追蹤 ss 命令的系統調用,便可快速獲悉 ss 的實現方式。老闆演示了 strace 命令的用法,很快就找到了 ss 的秘密 —— Netlink :

    $ strace ss -nat
    ...
    socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, NETLINK_SOCK_DIAG) = 3
    ...
    

    Netlink 套接字是 Linux 提供通訊機制,可用於內核與進程間、進程與進程間通訊。 Netlink 下的 sock_diag 子系統,提供了一種從內核獲取套接字信息的新方式。

    procfs 不同,sock_diag 採用網絡通訊的方式,內核作為服務端接收客戶端進程查詢請求,並以二進制數據包響應查詢結果,效率更高。

    這就是 ss 比 netstat 更快的原因, ss 採用 Netlink 機制,而 netstat 採用 procfs 機制。

    很不幸 Python 並沒有提供 Netlink API ,一般人可能又要干著急了。好在小菜先前有意識地研究了部分 Python 源碼,對 Python 的運行機制有所了解。

    他知道可以用 C 寫一個 Python 擴展模塊,在 C 語言中調用原生系統調用。

    編寫 Python C 擴展模塊可不簡單,對編程功底要求很高,必須全面掌握 Python 運行機制,特別是對象內存管理。

    一朝不慎可能導致程序異常退出、內存泄露等棘手問題。好在小菜已經不是當年的小菜了,他經受住了考驗。

    小菜的擴展模塊上線后,效果非常好,頂住了百萬級連接的採集壓力。

    一個看似簡單得不能再簡單的數據採集需求,背後涉及的知識可真不少,沒有一定的水平還真搞不定。好在小菜成長很快,他最終還是徹底地解決了性能問題,找回了久違的信心。

    內核模塊方案

    雖然性能問題已經徹底解決,小菜還是沒有將其淡忘。

    他時常想:如果可以將統計邏輯放在內核空間做,就不用在內核和進程之間傳遞大量連接信息了,效率應該是最高的!受限於當時的知識水平,小菜還沒有能力實現這個設想。

    後來小菜在研究 Linux 內核時,發現可以用內核模塊來擴展內核的功能,結合 procfs 的工作原理,他找到了技術方案!他順着 /proc/net/tcp 在內核中的實現源碼,依樣畫葫蘆寫了這個內核模塊:

    #include <linux/module.h>
    #include <linux/proc_fs.h>
    #include <linux/seq_file.h>
    #include <net/tcp.h>
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("Xiaocai");
    MODULE_DESCRIPTION("TCP state statistics");
    MODULE_VERSION("1.0");
    
    // 狀態名列表
    static char *state_names[] = {
        NULL,
        "ESTABLISHED",
        "SYN_SENT",
        "SYN_RECV",
        "FIN_WAIT1",
        "FIN_WAIT2",
        "TIME_WAIT",
        "CLOSE",
        "CLOSE_WAIT",
        "LAST_ACK",
        "LISTEN",
        "CLOSING",
        NULL
    };
    
    
    static void stat_sock_list(struct hlist_nulls_head *head, spinlock_t *lock,
        unsigned int state_counters[])
    {
        // 套接字節點指針(用於遍歷)
        struct sock *sk;
        struct hlist_nulls_node *node;
    
        // 鏈表為空直接返回
        if (hlist_nulls_empty(head)) {
            return;
        }
    
        // 自旋鎖鎖定
        spin_lock_bh(lock);
    
        // 遍歷套接字鏈表
        sk = sk_nulls_head(head);
        sk_nulls_for_each_from(sk, node) {
            if (sk->sk_state < TCP_MAX_STATES) {
                // 自增狀態計數器
                state_counters[sk->sk_state]++;
            }
        }
    
        // 自旋鎖解鎖
        spin_unlock_bh(lock);
    }
    
    
    static int tcpstat_seq_show(struct seq_file *seq, void *v)
    {
        // 狀態計數器
        unsigned int state_counters[TCP_MAX_STATES] = { 0 };
        unsigned int state;
    
        // TCP套接字哈希槽序號
        unsigned int bucket;
    
        // 先遍歷Listen狀態
        for (bucket = 0; bucket < INET_LHTABLE_SIZE; bucket++) {
            struct inet_listen_hashbucket *ilb;
    
            // 哈希槽
            ilb = &tcp_hashinfo.listening_hash[bucket];
    
            // 遍歷鏈表並統計
            stat_sock_list(&ilb->head, &ilb->lock, state_counters);
        }
    
        // 遍歷其他狀態
        for (bucket = 0; bucket < tcp_hashinfo.ehash_mask; bucket++) {
            struct inet_ehash_bucket *ilb;
            spinlock_t *lock;
    
            // 哈希槽鏈表
            ilb = &tcp_hashinfo.ehash[bucket];
            // 保護鎖
            lock = inet_ehash_lockp(&tcp_hashinfo, bucket);
    
            // 遍歷鏈表並統計
            stat_sock_list(&ilb->chain, lock, state_counters);
        }
    
        // 遍歷狀態輸出統計值
        for (state = TCP_ESTABLISHED; state < TCP_MAX_STATES; state++) {
            seq_printf(seq, "%-12s: %d\n", state_names[state], state_counters[state]);
        }
    
        return 0;
    }
    
    
    static int tcpstat_seq_open(struct inode *inode, struct file *file)
    {
        return single_open(file, tcpstat_seq_show, NULL);
    }
    
    
    static const struct file_operations tcpstat_file_ops = {
        .owner   = THIS_MODULE,
        .open    = tcpstat_seq_open,
        .read    = seq_read,
        .llseek  = seq_lseek,
        .release = single_release
    };
    
    
    static __init int tcpstat_init(void)
    {
        proc_create("tcpstat", 0, NULL, &tcpstat_file_ops);
        return 0;
    }
    
    
    static __exit void tcpstat_exit(void)
    {
        remove_proc_entry("tcpstat", NULL);
    }
    
    module_init(tcpstat_init);
    module_exit(tcpstat_exit);
    

    內核模塊編譯好並加載到內核后, procfs 文件系統提供了一個新文件 /proc/tcpstat ,內容為統計結果:

    $ cat /proc/tcpstat
    ESTABLISHED : 5
    SYN_SENT    : 0
    SYN_RECV    : 0
    FIN_WAIT1   : 0
    FIN_WAIT2   : 0
    TIME_WAIT   : 1
    CLOSE       : 0
    CLOSE_WAIT  : 0
    LAST_ACK    : 0
    LISTEN      : 14
    CLOSING     : 0
    

    當用戶程序讀取這個文件時,內核虛擬文件系統( VFS )調用小菜在內核模塊中寫的處理函數:遍歷內核 TCP 套接字完成統計並格式化統計結果。內核模塊、 VFS 以及套接字等知識超出專欄範圍,不再贅述。

    小菜在服務器上試驗這個內核模塊,真的快得飛起!

    經驗總結

    小菜開始總結這次腳本開發工作中的經驗教訓,他列出了以下關鍵節點:

    1. 依靠 psutil 採集,沒有關注 psutil 實現導致性能問題;
    2. 用生成器代替列表返回連接信息,解決內存瓶頸;
    3. 直接讀取 procfs 文件系統,部分解決 CPU 性能瓶頸;
    4. 通過調節 IO 緩衝區大小,進一步降低 CPU 開銷;
    5. Netlink 代替 procfs ,徹底解決性能問題;
    6. 實驗內核模塊思路,終極解決方案快得飛起;

    這些問題節點,一個比一個深入,沒有一定功底是搞不定的。小菜從剛開始跌跌撞撞,到後來獨當一面,快速成長的關鍵在於善於在問題中總結經驗教訓:

    • 程序開發完一定要做性能測試,看能夠扛住多大的壓力;
    • 使用任何工具,需要準確理解其背後的原理,避免誤用;
    • 對編程語言以及操作系統源碼要保持好奇心;
    • 計算機基礎知識很重要,需要及時補全才能達到新高度;
    • 學會問題發散,舉一反三;

    更多章節

    洞悉 Python 虛擬機運行機制,探索高效程序設計之道!

    到底如何才能提升我的 Python 開發水平,向更高一級的崗位邁進? 如果你有這些問題或者疑惑,請訂閱我們的專欄,閱讀更多章節:

    • 內建對象
    • 虛擬機
    • 函數機制
    • 類機制
    • 生成器與協程
    • 內存管理機制

    附錄

    更多 Python 技術文章請訪問:小菜學Python,轉至 原文 可獲得最佳閱讀體驗。

    訂閱更新,獲取更多學習資料,請關注 小菜學編程 :

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

    ※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

    南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

  • [C#.NET 拾遺補漏]05:操作符的幾個騷操作

    閱讀本文大概需要 1.5 分鐘。

    大家好,這是極客精神【C#.NET 拾遺補漏】專輯的第 5 篇文章,今天要講的內容是操作符。

    操作符的英文是 Operator,在數值計算中習慣性的被叫作運算符,所以在中文的概念中,運算符是操作符的一個子集。

    操作符是很基礎的知識了,基礎歸基礎,我們來回顧一下 C# 操作符那些比較騷的操作,能想到的不多,請大家補充。

    操作符的重載

    操作符重載大部分語言都沒有,而 C# 有。C# 允許用戶定義類型對操作符進行重載,方式是使用 operate 關鍵字把操作符寫成公開靜態函數。下面來演示一下重載 + 這個操作符。

    我們創建一個 Complex 結構類型來代表一個複數,我們知道複數有實數和虛數組成,於是可以這樣定義:

    public struct Complex
    {
        public double Real { get; set; }
        public double Imaginary { get; set; }
    }
    

    現在我們想實現複數的相加操作,即:

    Complex a = new Complex() { Real = 1, Imaginary = 2 };
    Complex b = new Complex() { Real = 4, Imaginary = 8 };
    Complex c = a + b;
    

    默認情況,自定義類是不能進行算術運算的,以上 a + b 會編譯報錯,我們需要對 + 進行操作符重載:

    public static Complex operator +(Complex c1, Complex c2)
    {
        return new Complex
        {
            Real = c1.Real + c2.Real,
            Imaginary = c1.Imaginary + c2.Imaginary
        };
    }
    

    C# 中像加減乘除等這類操作符都可以重載,也有些操作符是不能重載的,具體請查看文末參考鏈接。

    隱式和顯式轉換操作符

    我們知道子類可以隱式轉換為父類,在某種情況下(如父類由子類賦值而來)父類可以顯式轉換為子類。

    在 C# 中,對於沒有子父類關係的用戶定義類型,也是可以實現顯式和隱式轉換的。C# 允許用戶定義類型通過使用 implicitexplicit 關鍵字來控制對象的賦值和對象的類型轉換。它的定義形式如下:

    public static <implicit/explicit> operator <結果類型>(<源類型> myType)
    

    這裏以結果類型為方法名,源類型對象作為參數,只能是這一個參數,不能定義第二個參數,但可以通過該參數對象訪問其類的私有成員。下面是一個既有顯式又有隱式轉換操作符的例子:

    public class BinaryImage
    {
        private readonly bool[] _pixels;
    
        // 隱式轉換操作符示例
        public static implicit operator ColorImage(BinaryImage bm)
        {
            return new ColorImage(bm);
        }
    
        // 顯式轉換操作符示例
        public static explicit operator bool[](BinaryImage bm)
        {
            return bm._pixels;
        }
    }
    
    public class ColorImage
    {
        public ColorImage(BinaryImage bm) { }
    }
    

    這樣,我們就可以把 BinaryImage 對象隱式轉換為 ColorImage 對象,把 BinaryImage 對象顯式轉換為 bool 數組對象:

    var binaryImage = new BinaryImage();
    ColorImage colorImage = binaryImage; // 隱式轉換
    bool[] pixels = (bool[])binaryImage; // 顯式轉換
    

    而且轉換操作符可以定義為雙向显示和隱式轉換。既可從你的類型而來,亦可到你的類型而去:

    public class BinaryImage
    {
        public BinaryImage(ColorImage cm) { }
    
        public static implicit operator ColorImage(BinaryImage bm)
        {
            return new ColorImage(bm);
        }
    
        public static explicit operator BinaryImage(ColorImage cm)
        {
            return new BinaryImage(cm);
        }
    }
    

    我們知道 as 操作符也是一種顯式轉換操作符,那它適用於上面的這種情況嗎,即:

    ColorImage cm = myBinaryImage as ColorImage;
    

    你覺得這樣寫有問題嗎?請在評論區告訴我答案。

    空條件和空聯合操作符

    空條件(Null Conditional)操作符 ?. 和空聯合(Null Coalescing)操作符 ??,都是 C# 6.0 的語法,大多數人都很熟悉了,使用也很簡單。

    ?. 操作符會在對象為 null 時立即返回 null,不為 null 時才會調用後面的代碼。其中的符號 ? 代表對象本身,符號 . 代表調用,後面不僅可以是對象的屬性也可以是索引器或方法。以該操作符為分隔的每一截類型相同時可以接龍。示例:

    var bar = foo?.Value; // 相當於 foo == null ? null : foo.Value
    var bar = foo?.StringValue?.ToString(); // 每一截類型相同支持接龍
    var bar = foo?.IntValue?.ToString(); // 每一截類型不同,不能接龍,因為結果類型無法確定
    

    如果是調用索引器,則不需要符號 .,比如:

    var foo = new[] { 1, 2, 3 };
    var bar = foo?[1]; // 相當於 foo == null ? null : foo[1]
    

    空聯合操作符 ??,當左邊為空時則返回右邊的值,否則返回左邊的值。同樣,每一截的類型相同時支持接龍。

    var fizz = foo.GetBar() ?? bar;
    var buzz = foo ?? bar ?? fizz;
    

    => Lambda 操作符

    Lambda 操作符,即 =>,它用來定義 Lambda 表達式,也被廣泛用於 LINQ 查詢。它的一般定義形式如下:

    (input parameters) => expression
    

    示例:

    string[] words = { "cherry", "apple", "blueberry" };
    int minLength = words.Min((string w) => w.Length);
    

    實際應用中我們一般省略參數的類型聲明:

    int minLength = words.Min(w => w.Length);
    

    Lambda 操作符的後面可以是表達式,可以是語句,也可以是語句塊,比如:

    // 表達式
    (int x, int y) => x + y
    
    // 語句
    (string x) => Console.WriteLine(x)
    
    // 語句塊
    (string x) => {
        x += " says Hello!";
        Console.WriteLine(x);
    }
    
    

    這個操作符也可以很方便的用來定義委託方法(其實 Lambda 操作符就是由委託演變而來)。

    單獨定義委託方法:

    void MyMethod(string s)
    {
        Console.WriteLine(s + " World");
    }
    delegate void TestDelegate(string s);
    TestDelegate myDelegate = MyMethod;
    myDelegate("Hello");
    

    使用 Lambda 操作符:

    delegate void TestDelegate(string s);
    TestDelegate myDelegate = s => Console.WriteLine(s + " World");
    myDelegate("Hello");
    

    在一個類中,當實現體只有一句代碼時,也可以用 Lambda 操作符對方法和 Setter / Getter 進行簡寫:

    public class Test
    {
        public int MyProp { get => 123; }
        public void MyMethod() => Console.WriteLine("Hello!");
    }
    

    以上是幾種比較有代表性的操作符的“騷”操作,當然還有,但大多都過於基礎,大家都知道,就不總結了。

    C# 雖然目前不是最受歡迎的語言,但確實是一門優美的語言,其中少不了這些操作符語法糖帶來的功勞。

    參考:https://bit.ly/3h5yKNr

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

  • Uber基於Apache Hudi構建PB級數據湖實踐

    Uber基於Apache Hudi構建PB級數據湖實踐

    1. 引言

    從確保準確預計到達時間到預測最佳交通路線,在Uber平台上提供安全、無縫的運輸和交付體驗需要可靠、高性能的大規模數據存儲和分析。2016年,Uber開發了增量處理框架Apache Hudi,以低延遲和高效率為關鍵業務數據管道賦能。一年後,我們開源了該解決方案,以使得其他有需要的組織也可以利用Hudi的優勢。接着在2019年,我們履行承諾,進一步將其捐贈給了Apache Software Foundation,差不多一年半之後,Apache Hudi畢業成為Apache Software Foundation頂級項目。為紀念這一里程碑,我們想分享Apache Hudi的構建、發布、優化和畢業之旅,以使更大的大數據社區受益。

    2. 什麼是Apache Hudi

    Apache Hudi是一個存儲抽象框架,可幫助組織構建和管理PB級數據湖,通過使用upsert增量拉取等原語,Hudi將流式處理帶到了類似批處理的大數據中。這些功能通過統一的服務層(幾分鐘左右即可實現數據延遲),幫助我們更快,更新鮮地獲取服務數據,從而避免了維護多個系統的額外開銷。更靈活地,Apache Hudi還可以在Hadoop分佈式文件系統(HDFS)或雲存儲上運行。

    Hudi在數據湖上啟用原子性、一致性、隔離性和持久性(ACID)語義。 Hudi的兩個最廣泛使用的功能是upserts增量拉取,它使用戶能夠捕獲變更數據並將其應用於數據湖,為了實現這一點,Hudi提供了可插拔索引機制,以及自定義索引實現。Hudi具有控制和管理數據湖中文件布局的能力,這不僅能克服HDFS NameNode節點和其他雲存儲限制,而且對於通過提高可靠性和查詢性能來維護健康的數據生態系統也非常重要。另外Hudi支持多種查詢引擎,例如Presto,Apache Hive,Apache Spark和Apache Impala。

    圖1. Apache Hudi通過在表上提供不同的視圖來攝取變更日誌、事件和增量流,以服務於不同的應用場景

    從總體上講,Hudi在概念上分為3個主要組成部分:需要存儲的原始數據;用於提供upsert功能的索引數據以及用於管理數據集的元數據。內核方面,Hudi維護在不同時間點在表上執行的所有動作的時間軸,在Hudi中稱為即時,這提供了表格的即時視圖,同時還有效地支持了按序到達的數據檢索,Hudi保證時間軸上的操作是原子性的,並且基於即時時間,與數據庫中進行更改的時間是一致的。利用這些信息,Hudi提供了同一Hudi表的不同視圖,包括用於快速列式文件性能的讀優化視圖,用於快速數據攝取的實時視圖以及用於將Hudi表作為變更日誌流讀取的增量視圖,如上圖1所示。

    Hudi將數據表組織到分佈式文件系統上基本路徑(basepath)下的目錄結構中。 表分為多個分區,在每個分區內,文件被組織成文件組,由文件ID唯一標識。 每個文件組包含幾個文件切片,其中每個切片包含在某個特定提交/壓縮(commit/compaction)瞬間生成的基本數據文件(*.parquet),以及包含對基本數據文件進行插入/更新的一組日誌文件(*.log)。Hudi採用了Multiversion Concurrency Control(MVCC),其中壓縮操作將日誌和基本文件合併以生成新的文件片,而清理操作則將未使用的/較舊的文件片去除,以回收文件系統上的空間。

    Hudi支持兩種表類型:寫時複製和讀時合併。 寫時複製表類型僅使用列文件格式(例如,Apache Parquet)存儲數據。通過寫時複製,可以通過在寫過程中執行同步合併來簡單地更新版本並重寫文件。

    讀時合併表類型使用列式(例如Apache Parquet)和基於行(例如Apache Avro)文件格式的組合來存儲數據。 更新記錄到增量文件中,然後以同步或異步壓縮方式生成列文件的新版本。

    Hudi還支持兩種查詢類型:快照查詢和增量查詢。 快照查詢是從給定的提交或壓縮操作開始對錶進行”快照”的請求。利用快照查詢時,寫時複製表類型僅暴露最新文件片中的基本/列文件,並且與非Hudi表相比,可保證相同的列查詢性能。寫入時複製提供了現有Parquet表的替代品,同時提供了upsert/delete和其他功能。對於讀時合併表,快照查詢通過動態合併最新文件切片的基本文件和增量文件來提供近乎實時的數據(分鐘級)。對於寫時複製表,自給定提交或壓縮以來,增量查詢將提供寫入表的新數據,並提供更改流以啟用增量數據管道。

    3. Apache Hudi在Uber的使用

    在Uber,我們在各種場景中都使用到了Hudi,從在Uber平台上提供有關行程的快速、準確的數據,從檢測欺詐到在我們的UberEats平台上提供餐廳和美食推薦。為了演示Hudi的工作原理,讓我們逐步了解如何確保Uber Marketplace中的行程數據在數據湖上是最新的,從而改善Uber平台上的騎手和駕駛員的用戶體驗。行程的典型生命周期始於騎手提出的行程,然後隨着行程的進行而繼續,直到行程結束且騎手到達最終目的地時才結束。 Uber的核心行程數據以表格形式存儲在Uber的可擴展數據存儲Schemaless中。行程表中的單個行程條目在行程的生命周期中可能會經歷許多更新。在Uber使用Hudi之前,大型Apache Spark作業會定期將整個數據集重新寫入HDFS,以獲取上游在線表的插入、更新和刪除,從而反映出行程狀態的變化。就背景而言,在2016年初(在構建Hudi之前),一些最大的任務是使用1000個executors並處理超過20TB的數據,此過程不僅效率低下,而且難以擴展。公司的各個團隊都依靠快速、準確的數據分析來提供高質量的用戶體驗,為滿足這些要求,我們當前的解決方案無法擴展進行數據湖上的增量處理。使用快照和重新加載解決方案將數據移至HDFS時,這些低效率的處理正在寫到到所有數據管道,包括使用此原始數據的下游ETL,我們可以看到這些問題只會隨着規模的擴大而加劇。

    在沒有其他可行的開源解決方案可供使用的情況下,我們於2016年末為Uber構建並啟動了Hudi,以構建可促進大規模快速,可靠數據更新的事務性數據湖。Uber的第一代Hudi利用了寫時複製表類型,該表類型每30分鐘將作業處理速度提高到20GB,I/O和寫入放大減少了100倍。到2017年底,Uber的所有原始數據表都採用了Hudi格式,運行着地球上最大的事務數據湖之一。

    圖2. Hudi的寫時複製功能使我們能夠執行文件級更新,從而大大提高數據的新鮮度

    4. 改進Apache Hudi

    隨着Uber數據處理和存儲需求的增長,我們開始遇到Hudi的寫時複製功能的局限性,主要是需要繼續提高數據的處理速度和新鮮度,即使使用Hudi”寫時複製”功能,我們的某些表收到的更新也分散在90%的文件中,從而導致需要重寫數據湖中任何給定的大型表的數據,重寫數據量大約為100TB。由於寫時複製甚至為單個修改的記錄重寫整個文件,因此寫複製功能導致較高的寫放大和損害的新鮮度,從而導致HDFS群集上不必要的I/O以及更快地消耗磁盤空間,此外,更多的數據表更新意味着更多的文件版本,以及HDFS文件數量激增,反過來,這些需求導致HDFS Namenode節點不穩定和較高的計算成本。

    為了解決這些日益增長的擔憂,我們實現了第二種表類型,即”讀時合併”。由於讀時合併通過動態合併數據來使用近實時的數據,為避免查詢端的計算成本,我們需要合理使用此模式。”讀時合併”部署模型包括三個獨立的作業,其中包括一個攝取作業,包括由插入、更新和刪除組成的新數據,一個次要的壓縮作業,以異步方式主動地壓縮少量最新分區的更新/刪除內容,以及一個主要的壓縮作業,該作業會緩慢穩定地壓縮大量舊分區中的更新/刪除。這些作業中的每一個作業都以不同的頻率運行,次要作業和提取作業的運行頻率比主要作業要高,以確保其最新分區中的數據以列格式快速可用。通過這樣的部署模型,我們能夠以列式為數千個查詢提供新鮮數據,並將我們的查詢側合併成本限制在最近的分區上。使用讀時合併,我們能夠解決上面提到的所有三個問題,並且Hudi表幾乎不受任何對數據湖的更新或刪除的影響。現在,在Uber,我們會根據不同場景同時使用Apache Hudi的寫時複製和讀時合併功能。

    圖3. Uber的Apache Hudi團隊開發了一種數據壓縮策略,用於讀時合併表,以便頻繁將最近的分區轉化為列式存儲,從而減少了查詢端的計算成本

    有了Hudi,Uber每天向超過150PB數據湖中插入超過5,000億條記錄,每天使用30,000多個core,超過10,000多個表和數千個數據管道,Hudi每周在我們的各種服務中提供超過100萬個查詢。

    5. Apache Hudi經驗總結

    Uber在2017年開源了Hudi,為其他人帶來了該解決方案的好處,該解決方案可大規模提取和管理數據存儲,從而將流處理引入大數據。當Hudi畢業於Apache軟件基金會下的頂級項目時,Uber的大數據團隊總結了促使我們構建Hudi的各種考慮因素,包括:

    • 如何提高數據存儲和處理效率?
    • 如何確保數據湖包含高質量的表?
    • 隨着業務的增長,如何繼續大規模有效地提供低延遲的數據?
    • 在分鐘級別的場景中,我們如何統一服務層?

    如果沒有良好的標準化和原語,數據湖將很快成為無法使用的”數據沼澤”。這樣的沼澤不僅需要花費大量時間和資源來協調、清理和修復表,而且還迫使各個服務所有者構建複雜的算法來進行調整、改組和交易,從而給技術棧帶來不必要的複雜性。

    如上所述,Hudi通過無縫地攝取和管理分佈式文件系統上的大型分析數據集來幫助用戶控制其數據湖,從而彌補了這些差距。建立數據湖是一個多方面的問題,需要在數據標準化、存儲技術、文件管理實踐,數據攝取與數據查詢之間折衷性能等方面進行取捨。在我們建立Hudi時與大數據社區的其他成員交談時,我們了解到這些問題在許多工程組織中普遍存在。我們希望在過去的幾年中,開源和與Apache社區的合作,在Hudi基礎上發展可以使其他人在不同行業對大數據運營有更深入的了解。 在Uber之外,Apache Hudi已在多家公司用於生產,其中包括阿里雲,騰訊雲,AWS、Udemy等。

    6. 未來計劃

    圖4. Apache Hudi場景包括數據分析和基礎架構運行狀況監視

    Hudi通過對數據集強制schema,幫助用戶構建更強大、更新鮮的數據湖,從而提供高質量的見解。

    在Uber,擁有全球最大的事務數據湖之一為我們提供了各種Apache Hudi用例場景的機會,由於以這種規模解決問題並提高效率可能會產生重大影響,因此有直接的動機促使我們更加深入。在Uber,我們已經使用了先進的Hudi原語,如增量拉取來幫助建立鏈式增量流水線,從而減少了作業的計算空間,而這些作業本來會執行大型掃描和寫入。我們根據特定的用例場景和要求調整讀時合併表的壓縮策略。 自從我們將Hudi捐贈給Apache基金會以來,最近幾個月,Uber貢獻了一些功能,例如嵌入式時間軸服務以實現高效的文件系統訪問,刪除重命名以支持雲友好的部署並提高增量拉取性能。

    在接下來的幾個月中,Uber計劃為Apache Hudi社區貢獻更多新功能。其中一些功能可通過優化計算使用量以及改善數據應用程序的性能來幫助降低成本,我們還將更深入地研究如何根據訪問模式和數據應用程序需求來改善存儲管理和查詢性能。

    有關我們如何計劃實現這些目標的更多信息,您可以閱讀一些RFC,包括支持列索引和O(1)查詢計劃的智能元數據,將Parquet表高效引導到Hudi,記錄級別索引支持更快速插入,這些RFC由Uber的Hudi團隊向Apache社區提出。

    隨着Apache Hudi畢業成為Apache頂級項目,我們很高興為該項目雄心勃勃的路線圖做出貢獻。Hudi使Uber和其他公司可以使用開放源文件格式,在未來證明其數據湖的速度,可靠性和交易能力,從而消除了許多大數據挑戰,並構建了豐富而可移植的數據應用程序。

    Apache Hudi是一個成長中的社區,具有令人興奮且不斷髮展的發展路線圖。 如果您有興趣為這個項目做貢獻,可點擊這裏。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

    ※教你寫出一流的銷售文案?

  • 性能調優必備利器之 JMH

    性能調優必備利器之 JMH

    if 快還是 switch 快?HashMap 的初始化 size 要不要指定,指定之後性能可以提高多少?各種序列化方法哪個耗時更短?

    無論出自何種原因需要進行性能評估,量化指標總是必要的。

    在大部分場合,簡單地回答誰快誰慢是遠遠不夠的,如何將程序性能量化呢?

    這就需要我們的主角 JMH 登場了!

    JMH 簡介

    JMH(Java Microbenchmark Harness)是用於代碼微基準測試的工具套件,主要是基於方法層面的基準測試,精度可以達到納秒級。該工具是由 Oracle 內部實現 JIT 的大牛們編寫的,他們應該比任何人都了解 JIT 以及 JVM 對於基準測試的影響。

    當你定位到熱點方法,希望進一步優化方法性能的時候,就可以使用 JMH 對優化的結果進行量化的分析。

    JMH 比較典型的應用場景如下:

    1. 想準確地知道某個方法需要執行多長時間,以及執行時間和輸入之間的相關性
    2. 對比接口不同實現在給定條件下的吞吐量
    3. 查看多少百分比的請求在多長時間內完成

    下面我們以字符串拼接的兩種方法為例子使用 JMH 做基準測試。

    加入依賴

    因為 JMH 是 JDK9 自帶的,如果是 JDK9 之前的版本需要加入如下依賴(目前 JMH 的最新版本為 1.23):

    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-core</artifactId>
        <version>1.23</version>
    </dependency>
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-generator-annprocess</artifactId>
        <version>1.23</version>
    </dependency>
    

    編寫基準測試

    接下來,創建一個 JMH 測試類,用來判斷 +StringBuilder.append() 兩種字符串拼接哪個耗時更短,具體代碼如下所示:

    @BenchmarkMode(Mode.AverageTime)
    @Warmup(iterations = 3, time = 1)
    @Measurement(iterations = 5, time = 5)
    @Threads(4)
    @Fork(1)
    @State(value = Scope.Benchmark)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    public class StringConnectTest {
    
        @Param(value = {"10", "50", "100"})
        private int length;
    
        @Benchmark
        public void testStringAdd(Blackhole blackhole) {
            String a = "";
            for (int i = 0; i < length; i++) {
                a += i;
            }
            blackhole.consume(a);
        }
    
        @Benchmark
        public void testStringBuilderAdd(Blackhole blackhole) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < length; i++) {
                sb.append(i);
            }
            blackhole.consume(sb.toString());
        }
    
        public static void main(String[] args) throws RunnerException {
            Options opt = new OptionsBuilder()
                    .include(StringConnectTest.class.getSimpleName())
                    .result("result.json")
                    .resultFormat(ResultFormatType.JSON).build();
            new Runner(opt).run();
        }
    }
    

    其中需要測試的方法用 @Benchmark 註解標識,這些註解的具體含義將在下面介紹。

    在 main() 函數中,首先對測試用例進行配置,使用 Builder 模式配置測試,將配置參數存入 Options 對象,並使用 Options 對象構造 Runner 啟動測試。

    另外大家可以看下官方提供的 jmh 示例 demo:http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/

    執行基準測試

    準備工作做好了,接下來,運行代碼,等待片刻,測試結果就出來了,下面對結果做下簡單說明:

    # JMH version: 1.23
    # VM version: JDK 1.8.0_201, Java HotSpot(TM) 64-Bit Server VM, 25.201-b09
    # VM invoker: D:\Software\Java\jdk1.8.0_201\jre\bin\java.exe
    # VM options: -javaagent:D:\Software\JetBrains\IntelliJ IDEA 2019.1.3\lib\idea_rt.jar=61018:D:\Software\JetBrains\IntelliJ IDEA 2019.1.3\bin -Dfile.encoding=UTF-8
    # Warmup: 3 iterations, 1 s each
    # Measurement: 5 iterations, 5 s each
    # Timeout: 10 min per iteration
    # Threads: 4 threads, will synchronize iterations
    # Benchmark mode: Average time, time/op
    # Benchmark: com.wupx.jmh.StringConnectTest.testStringBuilderAdd
    # Parameters: (length = 100)
    

    該部分為測試的基本信息,比如使用的 Java 路徑,預熱代碼的迭代次數,測量代碼的迭代次數,使用的線程數量,測試的統計單位等。

    # Warmup Iteration   1: 1083.569 ±(99.9%) 393.884 ns/op
    # Warmup Iteration   2: 864.685 ±(99.9%) 174.120 ns/op
    # Warmup Iteration   3: 798.310 ±(99.9%) 121.161 ns/op
    

    該部分為每一次熱身中的性能指標,預熱測試不會作為最終的統計結果。預熱的目的是讓 JVM 對被測代碼進行足夠多的優化,比如,在預熱后,被測代碼應該得到了充分的 JIT 編譯和優化。

    Iteration   1: 810.667 ±(99.9%) 51.505 ns/op
    Iteration   2: 807.861 ±(99.9%) 13.163 ns/op
    Iteration   3: 851.421 ±(99.9%) 33.564 ns/op
    Iteration   4: 805.675 ±(99.9%) 33.038 ns/op
    Iteration   5: 821.020 ±(99.9%) 66.943 ns/op
    
    Result "com.wupx.jmh.StringConnectTest.testStringBuilderAdd":
      819.329 ±(99.9%) 72.698 ns/op [Average]
      (min, avg, max) = (805.675, 819.329, 851.421), stdev = 18.879
      CI (99.9%): [746.631, 892.027] (assumes normal distribution)
    
    Benchmark                               (length)  Mode  Cnt     Score     Error  Units
    StringConnectTest.testStringBuilderAdd       100  avgt    5   819.329 ±  72.698  ns/op
    

    該部分显示測量迭代的情況,每一次迭代都显示了當前的執行速率,即一個操作所花費的時間。在進行 5 次迭代后,進行統計,在本例中,length 為 100 的情況下 testStringBuilderAdd 方法的平均執行花費時間為 819.329 ns,誤差為 72.698 ns

    最後的測試結果如下所示:

    Benchmark                               (length)  Mode  Cnt     Score     Error  Units
    StringConnectTest.testStringAdd               10  avgt    5   161.496 ±  17.097  ns/op
    StringConnectTest.testStringAdd               50  avgt    5  1854.657 ± 227.902  ns/op
    StringConnectTest.testStringAdd              100  avgt    5  6490.062 ± 327.626  ns/op
    StringConnectTest.testStringBuilderAdd        10  avgt    5    68.769 ±   4.460  ns/op
    StringConnectTest.testStringBuilderAdd        50  avgt    5   413.021 ±  30.950  ns/op
    StringConnectTest.testStringBuilderAdd       100  avgt    5   819.329 ±  72.698  ns/op
    

    結果表明,在拼接字符次數越多的情況下,StringBuilder.append() 的性能就更好。

    生成 jar 包執行

    對於一些小測試,直接用上面的方式寫一個 main 函數手動執行就好了。

    對於大型的測試,需要測試的時間比較久、線程數比較多,加上測試的服務器需要,一般要放在 Linux 服務器里去執行。

    JMH 官方提供了生成 jar 包的方式來執行,我們需要在 maven 里增加一個 plugin,具體配置如下:

    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>2.4.1</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <finalName>jmh-demo</finalName>
                        <transformers>
                            <transformer
                                    implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                <mainClass>org.openjdk.jmh.Main</mainClass>
                            </transformer>
                        </transformers>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
    

    接着執行 maven 的命令生成可執行 jar 包並執行:

    mvn clean install
    java -jar target/jmh-demo.jar StringConnectTest
    

    JMH 基礎

    為了能夠更好地使用 JMH 的各項功能,下面對 JMH 的基本概念進行講解:

    @BenchmarkMode

    用來配置 Mode 選項,可用於類或者方法上,這個註解的 value 是一個數組,可以把幾種 Mode 集合在一起執行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime}),還可以設置為 Mode.All,即全部執行一遍。

    1. Throughput:整體吞吐量,每秒執行了多少次調用,單位為 ops/time
    2. AverageTime:用的平均時間,每次操作的平均時間,單位為 time/op
    3. SampleTime:隨機取樣,最後輸出取樣結果的分佈
    4. SingleShotTime:只運行一次,往往同時把 Warmup 次數設為 0,用於測試冷啟動時的性能
    5. All:上面的所有模式都執行一次

    @State

    通過 State 可以指定一個對象的作用範圍,JMH 根據 scope 來進行實例化和共享操作。@State 可以被繼承使用,如果父類定義了該註解,子類則無需定義。由於 JMH 允許多線程同時執行測試,不同的選項含義如下:

    1. Scope.Benchmark:所有測試線程共享一個實例,測試有狀態實例在多線程共享下的性能
    2. Scope.Group:同一個線程在同一個 group 里共享實例
    3. Scope.Thread:默認的 State,每個測試線程分配一個實例

    @OutputTimeUnit

    為統計結果的時間單位,可用於類或者方法註解

    @Warmup

    預熱所需要配置的一些基本測試參數,可用於類或者方法上。一般前幾次進行程序測試的時候都會比較慢,所以要讓程序進行幾輪預熱,保證測試的準確性。參數如下所示:

    1. iterations:預熱的次數
    2. time:每次預熱的時間
    3. timeUnit:時間的單位,默認秒
    4. batchSize:批處理大小,每次操作調用幾次方法

    為什麼需要預熱?

    因為 JVM 的 JIT 機制的存在,如果某個函數被調用多次之後,JVM 會嘗試將其編譯為機器碼,從而提高執行速度,所以為了讓 benchmark 的結果更加接近真實情況就需要進行預熱。

    @Measurement

    實際調用方法所需要配置的一些基本測試參數,可用於類或者方法上,參數和 @Warmup 相同。

    @Threads

    每個進程中的測試線程,可用於類或者方法上。

    @Fork

    進行 fork 的次數,可用於類或者方法上。如果 fork 數是 2 的話,則 JMH 會 fork 出兩個進程來進行測試。

    @Param

    指定某項參數的多種情況,特別適合用來測試一個函數在不同的參數輸入的情況下的性能,只能作用在字段上,使用該註解必須定義 @State 註解。

    在介紹完常用的註解后,讓我們來看下 JMH 有哪些陷阱。

    JMH 陷阱

    在使用 JMH 的過程中,一定要避免一些陷阱。

    比如 JIT 優化中的死碼消除,比如以下代碼:

    @Benchmark
    public void testStringAdd(Blackhole blackhole) {
        String a = "";
        for (int i = 0; i < length; i++) {
            a += i;
        }
    }
    

    JVM 可能會認為變量 a 從來沒有使用過,從而進行優化把整個方法內部代碼移除掉,這就會影響測試結果。

    JMH 提供了兩種方式避免這種問題,一種是將這個變量作為方法返回值 return a,一種是通過 Blackhole 的 consume 來避免 JIT 的優化消除。

    其他陷阱還有常量摺疊與常量傳播、永遠不要在測試中寫循環、使用 Fork 隔離多個測試方法、方法內聯、偽共享與緩存行、分支預測、多線程測試等,感興趣的可以閱讀 https://github.com/lexburner/JMH-samples 了解全部的陷阱。

    JMH 插件

    大家還可以通過 IDEA 安裝 JMH 插件使 JMH 更容易實現基準測試,在 IDEA 中點擊 File->Settings...->Plugins,然後搜索 jmh,選擇安裝 JMH plugin:

    這個插件可以讓我們能夠以 JUnit 相同的方式使用 JMH,主要功能如下:

    1. 自動生成帶有 @Benchmark 的方法
    2. 像 JUnit 一樣,運行單獨的 Benchmark 方法
    3. 運行類中所有的 Benchmark 方法

    比如可以通過右鍵點擊 Generate...,選擇操作 Generate JMH benchmark 就可以生成一個帶有 @Benchmark 的方法。

    還有將光標移動到方法聲明並調用 Run 操作就運行一個單獨的 Benchmark 方法。

    將光標移到類名所在行,右鍵點擊 Run 運行,該類下的所有被 @Benchmark 註解的方法都會被執行。

    JMH 可視化

    除此以外,如果你想將測試結果以圖表的形式可視化,可以試下這些網站:

    • JMH Visual Chart:http://deepoove.com/jmh-visual-chart/
    • JMH Visualizer:https://jmh.morethan.io/

    比如將上面測試例子結果的 json 文件導入,就可以實現可視化:

    總結

    本文主要介紹了性能基準測試工具 JMH,它可以通過一些功能來規避由 JVM 中的 JIT 或者其他優化對性能測試造成的影響。

    只需要將待測的業務邏輯用 @Benchmark 註解標識,就可以讓 JMH 的註解處理器自動生成真正的性能測試代碼,以及相應的性能測試配置文件。

    最好的關係就是互相成就,大家的在看、轉發、留言三連就是我創作的最大動力。

    參考

    http://openjdk.java.net/projects/code-tools/jmh/

    深入拆解Java虛擬機

    《實戰Java高併發程序設計》

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※別再煩惱如何寫文案,掌握八大原則!

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • 程序員過關斬將–作為一個架構師,我是不是應該有很多職責?

    每一個程序員都有一個架構夢。

    上面其實本質上是一句富有事實哲理的廢話,要不然也不會有這麼多人關注你的公眾號。這些年隨着“企業数字化”轉型的口號,一大批企業奔跑在轉型的路上,希望領先一步對手將企業IT部門從單純的成本中心轉變為業務驅動者,而這個過程中,企業的架構師起着舉足輕重的作用。架構師的工作在很多擼碼的開發者眼中是很一項很神聖的工作,而且富有挑戰性。

    但是事物都有兩面性,很多管理者和技術人員都認為架構師的薪酬不符合實際,有很多架構師確實只會用PPT和大幅海報來應付了事,而且會依仗着在公司地位把自己的一些想法強加給公司其他同事,有的架構師甚至會追求一些無關緊要的概念,在高層和底層灌輸一些錯誤的思想,從而導致做出一些不可逆轉的糟糕決策,使公司陷入危險逆境。

    很多時候,公司給予架構師這個角色太多的責任,管理者希望他們能在突發性能問題時能快速解決問題,還能推動企業快速轉型,甚至能幫助企業文化的快速建立,作為一個架構師是不是要抗下這些職責呢?

    我不是項目經理

    架構師的日常工作經常會面臨并行處理多個不同維度的問題,這些問題可能是不同的主題,甚至在做決策的時候也需要考慮人員的分配,項目時間表的排期,需要用的核心技術以及組件等。有很多高層領導喜歡直接在架構師這裏獲取項目的詳細信息以及技術方案,雖然架構師角色涉及這些信息並且很了解這些信息,但是這並不是架構師的職責所在,甚至很多情況下令架構師處於項目經理的尷尬角色。

    我不是開發人員

    我想很多人看過那篇文章:作為架構師該不該寫代碼?很多架構師是出身於開發人員,這也難怪會出現這樣的疑問。但是,架構師其實和資深開發是兩條不同的職業路線,我認為兩者沒有高低之分。出色的開發人員需要很深的開發功力,需要最終交付出可運行的軟件。而架構師則需要更廣闊的知識面,更好的組織戰略思想,更好的溝通能力。在一個產品的開發流水線上,架構師可能會負責一部分核心代碼的編寫,但是最主要的工作還是保證這條流水線的正常運轉。

    我不是救火員

    由於架構師這個角色在公司的地位,很多管理者認為架構師要隨時隨地的能分析並解決任何突發的問題,不瞞各位,這種現象在很多大廠依然存在,包括我司(雖然只是一個四線小廠)。如果一個架構師每天都忙着“救火”這種工作,根本沒有時間去做真正的架構工作,真正的架構設計需要思考,是不可能在短短時間內完成的。但是架構師必須接受出現的產品問題,因為這些問題的產生有可能和架構有着直接關係,在很大程度上能反應架構的缺陷或者問題。

    寫在最後

    架構師作為企業中很重要的一環,在很多重大技術問題中都作為決策者而存在。很難用代碼的多少或者質量來衡量一個架構師的好壞,如果一個系統在正常運行5年後依然能良好運行並且可以承受一定的變更能力,說明這個系統的架構師的工作是很出色的。如果非要給架構師定義一個KPI標準的話,以下這些工作也許能成為一個參考

    1. 定義IT戰略。小到一個系統的組件列表可行性的確定,大到公司技術的發展方向,乃至未來10年公司技術的預測與大膽嘗試。這些技術戰略都需要架構師根據自身經驗來制定。

    2. 落實對IT藍圖的管控,以實現協調一致,降低複雜度,保證公司所有系統有條不紊的正常工作,架構師的工作之一就是要把複雜度降低,化繁為簡,這需要架構師很強的抽象能力。

    3. 關注項目的實際落地情況,並根據項目實施中反饋的問題進行戰略的適當調整。一個合格的架構師從來不會忽略來自實際項目中的問題反饋。

    架構師一定要避免和消除那些系統設計中不可逆轉的錯誤決策

    來源參考:架構師應該知道的37件事

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

    ※教你寫出一流的銷售文案?

  • Autoware 標定工具 Calibration Tool Kit 聯合標定 Robosense-16 和 ZED 相機!

    Autoware 標定工具 Calibration Tool Kit 聯合標定 Robosense-16 和 ZED 相機!

    一、安裝 Autoware & ZED 內參標定 & 外參標定準備

    之前的這篇文章:Autoware 進行 Robosense-16 線雷達與 ZED 雙目相機聯合標定! 記錄了我用 Autoware 標定相機和雷達的過程,雖然用的不是 Calibration Tool Kit 工具,但是博客裏面的以下章節也適用本次的 Calibration Tool Kit :

    • 一、編譯安裝 Autoware-1.10.0
    • 二、標定 ZED 相機內參
    • 3.1 聯合標定準備

    如果你是第一次看這篇 Calibration Tool Kit 聯合標定的博客,建議先按照之前的博客安裝 Autoware、標定 ZED 內參和做好外參標定的準備(標定板,錄製標定包等),最好用上篇博客的方法標定一次。

    這篇博客我就直接開始介紹使用 Calibration Tool Kit 標定雷達和相機外參的過程!

    二、Calibration Tool Kit 聯合標定雷達和 ZED 相機

    2.1 啟動 Autoware

    先啟動 Autoware-1.10.0,啟動過程中可能需要輸入 root 密碼:

    # 1. 進入 autoware 的 ros 目錄下
    cd autoware-1.10.0/ros
    
    # 2. source 環境,zsh 或 bash
    source devel/setup.zsh[.bash]
    
    # 3. 啟動主界面
    ./run
    

    切換到 Sensing 選項卡:

    2.2 回放雷達相機 Bag

    這裏回放時需要更改雷達的話題為 /points_raw,因為這個工具訂閱的雷達主題是固定的:

    rosbag play --pause xxx.bag /rslidar_points:=/points_raw
    

    我用的 Robosense 雷達,發布的話題是 rslidar_points,這個回放默認暫停,防止跑掉數據,按空格繼續或暫停。

    2.3 啟動 Calibration Tool Kit

    點擊 Calibration Tool Kit 啟動標定工具:

    選擇圖像輸入話題,我只用的 ZED 的左圖像話題,如果沒有相機話題,確保前面你已經回放了 bag,選擇好了點擊 OK 確定:

    選擇標定類型為相機到 velodyne 雷達的標定(對 Robosense 雷達也適用,只不過需要更改點雲的發布話題),點擊 OK 確定:

    進入標定主界面 MainWindow:

    配置標定板棋盤格參數:

    • Pattern Size(m):標定板中每個格子的邊長,單位 m,我的標定板每個格子長 0.025 m
    • Pattern Number:標定板長X寬的單元格數量 – 1,我的標定板是長有 12 個格子,寬有 9 個,所以填 11×8,減一是因為標定檢測的是內部角點

    設置好了后,重啟 Calibration Tool Kit,點擊左上角 Load 導入第一步標定的相機內參 YAML 文件,但是這個工具只能導入 YML 格式的文件:

    因此需要把前面的內參標定文件拷貝一份,修改格式為 yml 即可,YAML 和 YML 其實是一樣的:

    修改好了之後,再點擊 Load 加載 yml 格式的內參文件即可:

    選擇不加載相機和雷達的標定數據,因為我是直接回放 Bag 標定:

    到這裏都設置好了,下面開始外參標定過程!

    2.4 標定過程

    打開回放 bag 終端,按空格繼續回放數據,主界面會显示相機圖像:

    但是右邊的點雲窗口沒有显示數據,需要我們調整視角才可以,視角的調整方法如下(文末有個 pdf 專門介紹):

    簡單解釋下,建議直接操作,很容易:

    • 移動點雲:上下左右方向鍵、PgUp、PgDn
    • 旋轉點雲:a、d、w、s、q、e
    • 切換模式:数字 1 和数字 2
    • 視角縮放:減號縮小、加號放大
    • 點雲大小:o 鍵使用小點雲、p 使用大點雲
    • 改變點雲窗口背景顏色:b

    我使用的使用直接按数字 2 切換模式就能看到點雲了,其實這些模式我也不是很懂。。。:

    如果需要更換背景,按 b 鍵改變為大致灰色即可:

    我這裏就不改背景了,黑色也挺好看出點雲的,然後使用上面的視角操作方法,把點雲中的標定板放大到中心位置:

    之後點擊右上角的 Grab 捕獲當前幀的圖像和點雲,使用 -+ 縮放視角:

    如果你點擊 grab 沒反應很正常,可能是棋盤格離得太遠或者模糊了,你多試幾個位置應該就能捕獲到,我回放一個 Bag 也就捕獲了 9 張左右。

    然後把鼠標放到右下角捕獲的點雲窗口,選擇一個棋盤格的中心位置區域,關於這個區域的選擇,我是參考這個標定工具的文檔例子(文末有鏈接)選擇的,大概就是標定板的中心位置選擇一個圓形的區域,盡量保證向外側的平面法向量垂直於標定板平面:

    鼠標左鍵點擊選擇,右鍵點擊取消,我的選擇如下,可以參考:

    然後重複以上步驟,不斷回放暫停,Grab 捕獲單幀圖像和點雲(多選一些),選擇點雲區域,直到回放結束,接着就可以點擊右上角的「Calibrate」按鈕計算外參矩陣(左上角显示),然後再點擊「Project」查看標定效果:

    切換左下方的單幀圖片和點雲窗口,捕獲的每一幀圖像和點雲都可以看到對齊效果,另外左邊也能看到標定的誤差,當然是越小越好,我目前的標定效果一般般,後續打算再標幾次。

    標定好之後,點擊左上角「save」保存外參矩陣即可,文件名建議帶上時間戳方便識別:

    最後的外參數文件如下,這個文件包含了相機內參和相機到雷達的外參:

    以上就是我的雷達相機聯合標定過程!希望能幫助正在標定雷達和相機的同學 ^_^!

    三、標定結果測試

    可以直接用之前博客 Autoware 進行 Robosense-16 線雷達與 ZED 雙目相機聯合標定! 中的「四、標定結果測試」一節介紹的步驟來測試融合效果:

    前幾天我把 ROS 的點雲和圖像的融合節點也調試好了,所以直接在程序裏面加載了外參矩陣,然後做了個初步的融合,效果如下:

    我也錄了個融合視頻,可以看看:B 站:Robosense-16 雷達與 ZED 相機數據融合。

    五、標定資源

    以下是我標定過程中收集的一些好的資料,這裏也分享給大家:

    • 標定工具的使用文檔在這裏:CalibrationToolkit_Manual.pdf
    • 這裏還有個視頻,有條件的同學可以看看:Yutobe:Autoware 標定相機和雷達

    另外 ROS 融合節點的程序我還在完善中,建議關注我的 Github 項目,後續會上傳節點代碼:AI-Notes: lidar_camera_fusion,如果標定遇到問題,可以公眾號後台給我發消息,或者直接在博客平台留言,我看到會儘快回復的,不過公眾號應該回復的快些,哈哈 :)

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※別再煩惱如何寫文案,掌握八大原則!

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • 本田摘得Google2016年度搜尋冠軍,繽智在其中扮演怎樣的角色?

    本田摘得Google2016年度搜尋冠軍,繽智在其中扮演怎樣的角色?

    繽智的內飾也凸顯着先鋒人群的獨到品味:充滿朝氣的撞色設計,搭配非對稱航空式座艙,這一設計巧思,頗受年輕人青睞,傳達出他們的脫俗格調。懸浮式儀錶台、多變魔術杯架等設計同樣極為出彩,縱享人性化舒適的同時,又展現了他們務實的另一面。

    據Google Trends近日公布的2016年度搜尋清單來看,在汽車品牌這一類別中,位列榜首的並非德系三強,而是一向以技術著稱的日系本田。更值得一提的是,Google今年採用了與去年不同的計算方式,既結合了去年的搜尋次數,亦結合了“最高搜索流量和最持久峰值搜索流量”的數據,由此產生的榜單含金量相比往年來得更高。如此看來,在高含金量的2016榜單之中,日系本田力壓奔馳、特斯拉等一眾豪華德系美系品牌獲此殊榮,更具說服力。

    SUV表現突出,本田關注節節攀升

    那麼,本田奪冠的奧秘又在哪裡?這與本田SUV車型的賣座息息相關。SUV車型無論在國內抑或是全球市場來說均為大熱車型,其中更以價格親民、功能實用的小型SUV尤為突出,而本田旗下的小型SUV—— HR-V(國內繽智)則在全球範圍內掀起了“本田熱”。作為一款全球車型,HR-V在國內有着一個消費者更為熟悉的名字——繽智。繽智採用了全球化的概念設計,其身為SUV的多面實用性、類Coupe的時尚外觀、MpV化的空間配置,都極為貼切的迎合了國內乃至國外車主的需求。

    憑藉全球車型的特殊身份,繽智在國內市場早已擁有較高人氣。以即將過去的2016年為例,廣汽本田繽智穩坐1至11月熱門合資車型銷量桂冠寶座,其中以11月銷量為例,繽智當月終端銷量高達17,485輛,同比增長45.1%,成績喜人的同時又不免讓人深思:繽智憑什麼打動眾多消費者的心?

    先鋒產品力 打造引領潮流的時尚座駕

    面對來自年輕化的受眾群體與瞬息萬變的時尚風向的雙重壓力,單純追逐潮流的設計已顯得有些捉襟見肘,在更多時候,只有引領潮流才能受人追捧持久不衰,而繽智就深知此道。在這個“看顏”的時代,繽智外觀具有天生優勢,更引領着年輕時尚人群對車型外觀的美學要求。

    那麼,繽智的獨特優勢又體現在何處呢?外觀整體以鑽石切面的幾何視覺效果為理念,頗為符合當下主流時尚圈審美;稜角分明的前臉與LED投影式前大燈組的組合極具時尚美學;側身上揚的腰線和隱藏式後門把手設計則頗有幾分Coupe風味,符合時下國際車型設計風向及國際化大眾口味。

    繽智的內飾也凸顯着先鋒人群的獨到品味:充滿朝氣的撞色設計,搭配非對稱航空式座艙,這一設計巧思,頗受年輕人青睞,傳達出他們的脫俗格調;懸浮式儀錶台、多變魔術杯架等設計同樣極為出彩,縱享人性化舒適的同時,又展現了他們務實的另一面。

    同時,繽智作為以黑科技著稱的廣本首款SUV車型,智能配置自然不容小覷,輕鬆滿足受眾需求。SmartEntry智能無匙進入系統輕鬆拉開車門坐進車內,按下一鍵啟動功能,懸浮式儀錶盤隨即點亮,盡顯科技感更充分調動了駕馭的熱情。與此同時,兼具手機屏幕映射功能的智能屏互聯繫統也十分便捷。

    追本溯源,無論一款車其他表現如何,與受眾匹配的動力系統都必須具備,而繽智要迎合的是對速度與駕馭快感追求都較高的年輕時尚人群,要求不可謂不高。繽智用銷量證明了自身的優異,以1.8L i-VTEC發動機打入市場,配合CVT無級變速器與AWD四驅系統,不但滿足了先鋒人士速度的追求又兼顧了良好的通過性。而之後上市的1.5L車型則搭載了地球夢科技發動機,不只滿足了年輕人不同的動力需求,官方給出的6.8L/100km的綜合油耗也滿足了年輕群體經濟環保願望,並由此受到了熱捧。

    廣本里程碑 繽智後市值得期待

    作為廣汽本田首款SUV車型,繽智自上市起便肩負廣汽本田打入國內SUV市場的重任。在上市短短2年時間里,繽智就以累計快高達30萬用戶,一舉成為廣汽本田旗下主打車型,同時更奪得前十一個月合資品牌小型SUV銷量冠軍。在幫助本田力壓其他日系對手成就日系在華銷量第一的同時,繽智早已不負眾望地扛起了廣汽本田SUV市場的大梁。

    在SUV市場如日中天的今天,繽智作為小型SUV的旗幟車型,憑藉潮流先鋒的外觀、品味獨到的內飾、科技感十足的配置與洶湧澎湃的動力,不只滿足了年輕人的多元化購車需求,更肩擔起了引領市場潮流風向的重任,如此看來,繽智獲得銷量冠軍是綜合實力的體現,帶動本田博得Google年度熱搜。

    再看此次Google榜單,奔馳、特斯拉位列二三,相比旗下GLC與Model 3等熱門車型,像國外的HR-V抑或是國內的繽智這樣的小型車型來說,更為親民的價格,同樣豐富的空間,以及不錯的通過性和全面的配置表現,都是其更為大熱的理由。今後也會有越來越多的年輕消費者會選擇經濟性更高的小型SUV,繽智作為熱門小型SUV,熱銷勢將繼續升溫。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

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

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

    ※教你寫出一流的銷售文案?

  • 7.98萬起產品力不輸哈弗H6,這款SUV車主是怎樣評價的?

    7.98萬起產品力不輸哈弗H6,這款SUV車主是怎樣評價的?

    最不滿意的地方:起步有點肉,而且這個時候發動機聲音是比較大的。所以感覺上這個發動機實際動力還是比較一般的,而且發動機倉隔音也是比較一般。車主:老兵哥購買車型:北汽幻速S6 2017款 1。5T CVT樂享型裸車購買價:10。

    前言

    作為國內的造車大戶,北汽奉行着“多生孩兒多掙錢”的政策,有注重一般家用市場的北汽紳寶、有注重硬派SUV市場的北京汽車、還有着北汽幻速以及北汽比速這兩個入門品牌。那麼作為一個比較年輕的品牌,北汽幻速的口碑究竟怎樣呢?今天筆者就搜集了幾位北汽幻速S6車主的意見,這款7.98萬起售的緊湊型SUV有着該價位中不俗的競爭力。全系標配的是1.5T發動機,而且有着CVT變速箱作為自動擋的選擇。

    那麼多孩子,打起群架肯定贏

    北汽銀翔幻速S6

    官方指導價:7.98-11.68萬

    編者意見:

    性價比較高,動力表現在同價位中表現比較優秀。不過全系沒能標配ESp車身穩定系統以及电子助力轉向比較可惜。

    車主:BY2000

    購買車型:北汽幻速S6 2017款 1.5T CVT尊享型

    裸車購買價11.68 萬元

    最滿意的地方:整體都比較滿意,但是最滿意的的是價格,性價比很高。能在這個價格買到這樣配置的緊湊型SUV還是不錯的,而且是渦輪增壓發動機。

    最不滿意的地方:起步有點肉,而且這個時候發動機聲音是比較大的。所以感覺上這個發動機實際動力還是比較一般的,而且發動機倉隔音也是比較一般。

    車主:老兵哥

    購買車型:北汽幻速S6 2017款 1.5T CVT樂享型

    裸車購買價:10.68 萬元

    最滿意的地方:乘坐空間,沒有想到這個價格的車還能有着那麼大的空間,滿載的情況下也不是很擁擠。而且後備箱容積也是相當可觀,大天窗還有那麼多的配置,買這款車真的是比較值。

    最不滿意的地方:裝配工藝有待加強,有些部分的縫隙是比較大的,就如尾門的縫隙,看着很掉價,而且方向盤塑料感太強了。

    車主:smg20900

    購買車型:北汽幻速S6 2017款 1.5T CVT尊享型

    裸車購買價:10.68 萬元

    最滿意的地方:外觀,看着更像是二十多萬的SUV。能給人更多的面子,而且在動力方面感覺還是不錯的,一直都可以維持在較低轉速,120km/h時速下轉速也只是2200rpm左右,這個是最為滿意的,所以綜合油耗上也是9L左右,對於一款SUV來說是滿意了。

    最不滿意的地方:儲物空間實在是少得可憐,中間只有一個杯架,不夠用。其次是噪音的問題,不過對於如此便宜的車來說,還是可以接受的。

    編者總結:

    北汽雖然是個歷史悠長的品牌,但事實基本是為別人“代工”,自身在工藝方面以及控製成本方面還是需要向合資學習,所以在做工以及隔音用料上表現一般。不過在主要的發動機上表現卻是相當不錯,雖然渦輪遲滯現象還是有的,但是油耗表現卻是令人信服的。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※台北網頁設計公司全省服務真心推薦

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※推薦評價好的iphone維修中心