分類
發燒車訊

一分鐘帶你了解下Spring Security!

一、什麼是Spring Security?

Spring Security是一個功能強大且高度可定製的身份驗證和訪問控制框架,它是用於保護基於Spring的應用程序的實際標準。

Spring Security是一個框架,致力於為Java應用程序提供身份驗證和授權。與所有Spring項目一樣,Spring Security的真正強大之處在於可以輕鬆擴展以滿足自定義要求。

更多信息可以查看官網:https://spring.io/projects/spring-security

二、Spring Security的主要功能

  • 認證:驗證用戶名和密碼是否合法(是否系統中用戶)
  • 授權:是系統用戶不代表你能使用某些功能,因為你可能沒有權限
  • 防禦會話固定,點擊劫持,跨站點請求偽造等攻擊
  • Servlet API集成
  • 與Spring Web MVC的可選集成

三、快速入門

新建一個SpringBoot的web項目spring-boot-security。

案例1:接口不添加保護

pom文件中不引入Spring Security,然後新建一個controller:

@RestController
public class AppController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello,spring security!";
    }
}

然後打開瀏覽器訪問:http://localhost:8080/hello,成功后返回:

Hello,spring security!

案例2:接口添加保護

  1. pom文件添加依賴

pom文件中引入Spring Security的starter:

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
  1. 訪問接口

打開瀏覽器再次訪問http://localhost:8080/hello,會被重定向到登錄頁http://localhost:8080/login,截圖如下:

要登錄系統,我們需要知道用戶名和密碼,Spring Security默認的用戶名是user,項目啟動的時候會生成默認密碼(在啟動日誌中可以看到),輸入用戶名和密碼后就可以訪問/hello接口了。

當然也可以自定義用戶名密碼,在配置文件添加如下內容即可:

spring.security.user.name=java_suisui
spring.security.user.password=123456

四、自定義認證和授權

上面說過Spring Security的功能有“認證”和“授權”,下面通過一個簡單的例子實現下自定義的認證和授權。

假設系統中有兩個角色:

  • ADMIN 可以訪問/admin下的資源
  • USER 可以訪問/user下的資源

按照下面步驟操作即可。

  1. 新建一個配置類

對於用戶名、密碼、登錄頁面、訪問權限等都可以在 WebSecurityConfigurerAdapter 的實現類中配置。

WebSecurityConfig代碼如下:

/**
 * 配置類
 * @Author java_suisui
 *
 */
@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //配置內存中的 用戶名、密碼和角色
        auth.inMemoryAuthentication().passwordEncoder(new MyPasswordEncoder()).withUser("user").password("123456").roles("USER");
        auth.inMemoryAuthentication().passwordEncoder(new MyPasswordEncoder()).withUser("admin").password("123456").roles("ADMIN");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login").permitAll()
                .antMatchers("/user").hasRole("USER") //訪問 /user這個接口,需要有USER角色
                .antMatchers("/admin").hasRole("ADMIN")
                .anyRequest().authenticated() //剩餘的其他接口,登錄之後就能訪問
                .and()
                .formLogin().defaultSuccessUrl("/hello");
    }
}
  1. 創建PasswordEncorder的實現類

內存用戶驗證時,Spring Boot 2.0以上版本引用的security 依賴是 spring security 5.X版本,此版本需要提供一個PasswordEncorder的實例。

MyPasswordEncoder代碼如下:

public class MyPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence rawPassword) {
        return rawPassword.toString();
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(rawPassword);
    }
}
  1. 登錄驗證

瀏覽器打開http://localhost:8080/login,

  • 使用user登錄,可以訪問/user
  • 使用admin登錄,可以訪問/admin

如果使用user登錄后訪問/admin,會報403錯誤,具體錯誤信息如下:

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Tue Nov 19 16:26:28 CST 2019
There was an unexpected error (type=Forbidden, status=403).
Forbidden

結果和我們預期的一致,說明簡單的自定義認證和授權功能已經實現了。

完整源碼地址:

推薦閱讀

Java碎碎念,一個堅持原創的公眾號,為您提供一系列系統架構、微服務、Java、SpringBoot、SpringCloud等高質量技術文章。
如果覺得文章不錯,希望可以隨手轉發或者”在看“哦,非常感謝哈!
關注下方公眾號后回復「1024」,有驚喜哦!

本文由博客一文多發平台 發布!

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

分類
發燒車訊

在 ASP.NET Core 項目中使用 MediatR 實現中介者模式

 一、前言

   最近有在看 DDD 的相關資料以及微軟的  這個項目中基於 DDD 的架構設計,在  這個示例服務中,可以看到各層之間的代碼調用與我們之前傳統的調用方式似乎差異很大,整個項目各個層之間的代碼全部是通過注入 IMediator 進行調用的,F12 查看源碼后可以看到該接口是屬於  這個組件的。既然要照葫蘆畫瓢,那我們就先來了解下如何在 ASP.NET Core 項目中使用 。

  代碼倉儲:

 二、Step by Step

    從 github 的項目主頁上可以看到作者對於這個項目的描述是基於中介者模式的 .NET 實現,是一種基於進程內的數據傳遞。也就是說這個組件主要實現的是在一個應用中實現數據傳遞,如果想要實現多個應用間的數據傳遞就不太適合了。從作者的 github 個人主頁上可以看到,他還是  這個 OOM 組件的作者,PS,如果你想要了解如何在 ASP.NET Core 項目中使用 AutoMapper,你可以查看我之前寫的這一篇文章()。而對於 MediatR 來說,在具體的學習使用之前,我們先來了解下什麼是中介者模式。

  1、什麼是中介者模式

  很多舶來詞的中文翻譯其實最終都會與實際的含義相匹配,例如軟件開發過程中的 23 種設計模式的中文名稱,我們其實可以比較容易的從中文名稱中得知出該設計模式具體想要實現的作用,就像這裏介紹的中介者模式。

  在我們通過代碼實現實際的業務邏輯時,如果涉及到多個對象類之間的交互,通常我們都是會採用直接引用的形式,隨着業務邏輯變的越來越複雜,對於一個簡單的業務抽象出的實現方法中,可能會被我們添加上各種判斷的邏輯或是對於數據的業務邏輯處理方法。

  例如一個簡單的用戶登錄事件,我們可能最終會抽象出如下的業務流程實現。

public bool Login(AppUserLoginDto dto, out string msg)
{
    bool flag = false;
    try
    {
        // 1、驗證碼是否正確
        flag = _redisLogic.GetValueByKey(dto.VerificationCode);
        if (!flag)
        {
            msg = "驗證碼錯誤,請重試";
            return false;
        }

        // 2、驗證賬戶密碼是否正確
        flag = _userLogic.GetAppUser(dto.Account.Trim(), dto.Password.Trim(), out AppUserDetailDto appUser);
        if (!flag)
        {
            msg = "賬戶或密碼錯誤,請重試";
            return false;
        }

        // 3、驗證賬戶是否可以登錄當前的站點(未被鎖定 or 具有登錄當前系統的權限...)
        flag = _authLogic.CheckIsAvailable(appUser);
        if (!flag)
        {
            msg = "用戶被禁止登錄當前系統,請重試";
            return false;
        }

        // 4、設置當前登錄用戶信息
        _authLogic.SetCurrentUser(appUser);

        // 5、記錄登錄記錄
        _userLogic.SaveLoginRecord(appUser);

        msg = "";
        return true;
    }
    catch (Exception ex)
    {
        // 記錄錯誤信息
        msg = $"用戶登錄失敗:{ex.Message}";
        return false;
    }
}

  這裏我們假設對於登錄事件的實現方法存在於 UserAppService 這個類中,對於 redis 資源的操作在 RedisLogic 類中,對於用戶相關資源的操作在 UserLogic 中,而對於權限校驗相關的資源操作位於 AuthLogic 類中。

  可以看到,為了實現 UserAppService 類中定義的登錄方法,我們至少需要依賴於 RedisLogic、UserLogic 以及 AuthLogic,甚至在某些情況下可能在 UserLogic 和 AuthLogic 之間也存在着某種依賴關係,因此我們可以從中得到如下圖所示的類之間的依賴關係。

  一個簡單的登錄業務尚且如此,如果我們需要對登錄業務添加新的需求,例如現在很多網站的登錄和註冊其實是放在一起的,當登錄時如果判斷沒有當前的用戶信息,其實會催生創建新用戶的流程,那麼,對於原本的登錄功能實現,是不是會存在繼續添加新的依賴關係的情況。同時對於很多本身就很複雜的業務,最終實現出來的方法是不是會有更多的對象類之間存在各種的依賴關係,牽一發而動全身,後期修改測試的成本會不會變得更高。

  那麼,中介者模式是如何解決這個問題呢?

  在上文有提到,對於舶來詞的中文名稱,中文更多的會根據實際的含義進行命名,試想一下我們在現實生活中提到中介,是不是更多的會想到房屋中介這一角色。當我們來到一個新的城市,面臨着租房的問題,絕大多數的情況下,我們最終需要通過中介去達成我們租房的目的。在租房這個案例中,房屋中介其實就是一个中介者,他承接我們對於想要租的房子的各種需求,從自己的房屋數據庫中去尋找符合條件的,最終以一個橋樑的形式,連接我們與房東,最終就房屋的租住達成一致。

  而在軟件開發中,中介者模式則是要求我們根據實際的業務去定義一個包含各種對象之間交互關係的對象類,之後,所有涉及到該業務的對象都只關聯於這一个中介對象類,不再顯式的調用其它類。採用了中介者模式之後設計的登錄功能所涉及到的類依賴如下圖所示,這裏的 AppUserLoginEventHandler 其實就是我們的中介類。

  當然,任何事都會有利有弊,不會存在百分百完美的事情,就像我們通過房租中介去尋找合適的房屋,最終我們需要付給中介一筆費用去作為酬勞,採用中介者模式設計的代碼架構也會存在別的問題。因為在代碼中引入了中介者這一對象,勢必會增加我們代碼的複雜度,可能會使原本很輕鬆就實現的代碼變得複雜。同時,我們引入中介者模式的初衷是為了解決各個對象類之間複雜的引用關係,對於某些業務來說,本身就很複雜,最終必定會導致這个中介者對象異常複雜。

  畢竟,軟件開發的過程中不會存在銀彈去幫我們解決所有的問題。

  那麼,在本篇文章的示例代碼中,我將使用 MediatR 這一組件,通過引入中介者模式的思想來完成上面的用戶登錄這一案例。

  2、組件加載

  在使用 MediatR 之前,這裏簡單介紹下這篇文章的示例 demo 項目。這個示例項目的架構分層可以看成是介於傳統的多層架構與採用 DDD 的思想的架構分層。嗯,你可以理解成四不像,屬於那種傳統模式下的開發人員在往 DDD 思想上進行遷移的成品,具體的代碼分層說明解釋如下。

  01_Infrastructure:基礎架構層,這層會包含一些對於基礎組件的配置或是幫助類的代碼,對於每個新建的服務來說,該層的代碼幾乎都是差不多的,所以對於基礎架構層的代碼其實最好是發布到公有 or 私有的 Nuget 倉庫中,然後我們直接在項目中通過 Nuget 去引用。

  對於採用 DDD 的思想構建的項目來說,很多人可能習慣將一些實體的配置也放置在基礎架構層,我的個人理解還是應該置於領域層,對於基礎架構層,只做一些基礎組件的封裝。如果有什麼不對的地方,歡迎在評論區提出。

  02_Domain:領域層,這層會包含我們根據業務劃分出的領域的幾乎所有重要的部分,有領域對象(Domain Object)、值對象(Value Object)、領域事件(Domain Event)、以及倉儲(Repository)等等領域組件。

  這裏雖然我創建了 AggregateModels(聚合實體)這個文件夾,其實在這個項目中,我創建的還是不包含任何業務邏輯的貧血模型。同時,對於倉儲(Repository)在領域分層中是置於 Infrastructure(基礎架構層)還是位於 Domain(領域層),每個人都會有自己的理解,這裏我還是更傾向於放在 Domain 層中更符合其定位。

  03_Application:應用層,這一層會包含我們基於領域所封裝出的各種實際的業務邏輯,每個封裝出的服務應用之間並不會出現互相調用的情況。

  Sample.Api:API 接口層,這層就很簡單了,主要是通過 API 接口暴露出我們基於領域對外提供的各種服務。

  整個示例項目的分層結構如下圖所示。

  與使用其它的第三方組件的使用方式相同,在使用之前,我們需要在項目中通過 Nuget 添加對於 MediatR 的程序集引用。

  這裏需要注意,因為我們主要是通過引用 MediatR 來實現中介者模式,所以我們只需要在領域層和應用層加載 MediatR 即可。而對於 Sample.Api 這個 Web API 項目,因為需要通過依賴注入的方式來使用我們基於 MediatR 所構建出的各種服務,所以這裏我們還要添加 MediatR.Extensions.Microsoft.DependencyInjection 這個程序集到 Sample.Api 中。

Install-Package MediatR
Install-Package MediatR.Extensions.Microsoft.DependencyInjection

  3、案例實現

  首先我們在 Sample.Domain 這個類庫的 AggregateModels 文件夾下添加 AppUser(用戶信息)類 和 Address(地址信息) 類,這裏雖然並沒有採用 DDD 的思想去劃分領域對象和值對象,我們創建出來的都是不含任何業務邏輯的貧血模型。但是在用戶管理這個業務中,對於用戶所包含的聯繫地址信息,其實是一種無狀態的數據。也就是說對於同一個地址信息,不會因為置於多個用戶中而出現數據的二義性。因此,對於地址信息來說,是不需要唯一的標識就可以區分出這個數據的,所以這裏的 Address 類就不需要添加主鍵,其實也就是對應於領域建模中的值對象。

  這裏我是使用的 EF Core 作為項目的 ORM 組件,當創建好需要使用實體之後,我們在 Sample.Domain 這個類庫下面新建一個 SeedWorks 文件夾,添加自定義的 DbContext 對象和用於執行 EF Core 第一次生成數據庫時寫入預置種子數據的信息類。

  這裏需要注意,在 EF Core 中,當我們需要將編寫的 C# 類通過 Code First 創建出數據庫表時,我們的 C# 類必須包含主鍵信息。而對應到我們這裏的 Address 類來說,它更多的是作為 AppUser 類中的屬性信息來展示的,所以這裏我們需要對 EF Core 生成數據庫表的過程進行重寫。

  這裏我們在 SeedWorks 文件夾下創建一個新的文件夾 EntityConfigurations,在這裏用來存放我們自定義的 EF Core 創建表的規則。新建一個繼承於 IEntityTypeConfiguration<AppUser> 接口的 AppUserConfiguration 配置類,在接口默認 Configure 方法中,我們需要編寫映射規則,將 Address 類作為 AppUser 類中的字段進行显示,最終實現后的代碼如下所示。 

public class AppUserConfiguration : IEntityTypeConfiguration<AppUser>
{
    public void Configure(EntityTypeBuilder<AppUser> builder)
    {
        // 表名稱
        builder.ToTable("appuser");

        // 實體屬性配置
        builder.OwnsOne(i => i.Address, n =>
        {
            n.Property(p => p.Province).HasMaxLength(50)
                .HasColumnName("Province")
                .HasDefaultValue("");

            n.Property(p => p.City).HasMaxLength(50)
                .HasColumnName("City")
                .HasDefaultValue("");

            n.Property(p => p.Street).HasMaxLength(50)
                .HasColumnName("Street")
                .HasDefaultValue("");

            n.Property(p => p.ZipCode).HasMaxLength(50)
                .HasColumnName("ZipCode")
                .HasDefaultValue("");
        });
    }
}

  當創建表的映射規則編寫完成后,我們就可以對 UserApplicationDbContext 類進行重寫 OnModelCreating 方法。在這個方法中,我們就可以去應用我們自定義設置的實體映射規則,從而讓 EF Core 按照我們的想法去創建數據庫,最終實現的代碼如下所示。

public class UserApplicationDbContext : DbContext
{
    public DbSet<AppUser> AppUsers { get; set; }

    public UserApplicationDbContext(DbContextOptions<UserApplicationDbContext> options)
        : base(options)
    {
    }

    /// <summary>
    ///
    /// </summary>
    /// <param name="modelBuilder"></param>
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 自定義 AppUser 表創建規則
        modelBuilder.ApplyConfiguration(new AppUserConfiguration());
    }
}

  當我們創建好 DbContext 后,我們需要在 Startup 類的 ConfigureServices 方法中進行注入。在示例代碼中,我使用的是 MySQL 8.0 數據庫,將配置文件寫入到 appsettings.json 文件中,最終注入 DbContext 的代碼如下所示。

public void ConfigureServices(IServiceCollection services)
{
    // 配置數據庫連接字符串
    services.AddDbContext<UserApplicationDbContext>(options =>
        options.UseMySql(Configuration.GetConnectionString("SampleConnection")));
}

  數據庫的連接字符串配置如下。

{
  "ConnectionStrings": {
    "SampleConnection": "server=127.0.0.1;database=sample.application;user=root;password=123456@sql;port=3306;persistsecurityinfo=True;"
  }
}

  在上文有提到,除了創建一個 DbContext 對象,我們還創建了一個 DbInitializer 類用於在 EF Core 第一次執行創建數據庫操作時將我們預置的信息寫入到對應的數據庫表中。這裏我們只是簡單的判斷下 AppUser 這張表是否存在數據,如果沒有數據,我們就添加一條新的記錄,最終實現的代碼如下所示。

public class DbInitializer
{
    public static void Initialize(UserApplicationDbContext context)
    {
        context.Database.EnsureCreated();

        if (context.AppUsers.Any())
            return;

        AppUser admin = new AppUser()
        {
            Id = Guid.NewGuid(),
            Name = "墨墨墨墨小宇",
            Account = "danvic.wang",
            Phone = "13912345678",
            Age = 12,
            Password = "123456",
            Gender = true,
            IsEnabled = true,
            Address = new Address("啦啦啦啦街道", "啦啦啦市", "啦啦啦省", "12345"),
            Email = "danvic.wang@yuiter.com",
        };

        context.AppUsers.Add(admin);
        context.SaveChanges();
    }
}

  當我們完成種子數據植入的代碼,我們需要在程序啟動之前就去執行我們的代碼。因此我們需要修改 Program 類中的 Main 方法,實現在運行 web 程序之前去執行種子數據的植入。

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateWebHostBuilder(args).Build();

        using (var scope = host.Services.CreateScope())
        {
            // 執行種子數據植入
            //
            var services = scope.ServiceProvider;
            var context = services.GetRequiredService<UserApplicationDbContext>();
            DbInitializer.Initialize(context);
        }
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

  這時,運行我們的項目,程序就會自動執行創建數據庫的操作,同時會將我們預設好的種子數據寫入到數據庫表中,最終實現的效果如下圖所示。

  基礎的項目代碼已經完成之後,我們就可以開始學習如何通過 MediatR 來實現中介者模式。在這一章的示例項目中,我們會使用到 MediatR 中兩個很重要的接口類型:IRequest 和 INotification。

  在 Github 上,作者針對這兩個接口做了如下的,這裏我會按照我的理解去進行使用。同時,為了防止我的理解出現了偏差,從而對各位造成影響,這裏貼上作者回復解釋的原文。

Requests are for:
1 request to 1 handler. Handler may or may not return a value
Notifications are for:
1 notification to n handlers. Handler may not return a value.


In practical terms, requests are "commands", notifications are "events".
Command would be directing MediatR to do something like "ApproveInvoiceCommand -> ApproveInvoiceHandler". Event would be
notifications, like "InvoiceApprovedEvent -> SendThankYouEmailToCustomerHandler"

 

  對於繼承於 IRequest 接口的類來說,一個請求(request)只會有一個針對這個請求的處理程序(requestHandler),它可以返回值或者不返回任何信息;

  而對於繼承於 INotification 接口的類來說,一個通知(notification)會對應多個針對這個通知的處理程序(notificationHandlers),而它們不會返回任何的數據。

  請求(request)更像是一種命令(command),而通知(notification)更像是一種事件(event)。嗯,可能看起來更暈了,jbogard 這裏給了一個案例給我們進一步的解釋了 request 與 notification 之間的差異性。

  雙十一剛過,很多人都會瘋狂剁手,對於購買大件來說,為了能夠更好地擁有售後服務,我們在購買后肯定會期望商家給我們提供發票,這裏的要求商家提供發票就是一種 request,而針對我們的這個請求,商家會做出回應,不管能否開出來發票,商家都應當通知到我們,這裏的通知用戶就是一種 notification。

  對於提供發票這個 request 來說,不管最終的結果如何,它只會存在一種處理方式;而對於通知用戶這個 notification 來說,商家可以通過短信通知,可以通過公眾號推送,也可以通過郵件通知,不管採用什麼方式,只要完成了通知,對於這個事件來說也就已經完成了。    

  而對應於用戶登錄這個業務來說,用戶的登錄行為其實就是一個 request,對於這個 request 來說,我們可能會去數據庫查詢賬戶是否存在,判斷是不是具有登錄系統的權限等等。而不管我們在這個過程中做了多少的邏輯判斷,它只會有兩種結果,登錄成功或登錄失敗。而對於用戶登錄系統之後可能需要設置當前登錄人員信息,記錄用戶登錄日誌這些行為來說,則是歸屬於 notification 的。

  弄清楚了用戶登錄事件中的 request 和 notification 劃分,那麼接下來我們就可以通過代碼來實現我們的功能。這裏對於示例項目中的一些基礎組件的配置我就跳過了,如果你想要具體的了解這裏使用到的一些組件的使用方法,你可以查閱我之前的文章。

  首先,我們在 Sample.Application 這個類庫下面創建一個 Commands 文件夾,在下面存放用戶的請求信息。現在我們創建一個用於映射用戶登錄請求的 UserLoginCommand 類,它需要繼承於 IRequest<T> 這個泛型接口。因為對於用戶登錄這個請求來說,只會有可以或不可以這兩個結果,所以對於這個請求的響應的結果是 bool 類型的,也就是說,我們具體應該繼承的是 IRequest<bool>。

  對於用戶發起的各種請求來說,它其實只是包含了對於這次請求的一些基本信息,而對於 UserLoginCommand 這個用戶登錄請求類來說,它可能只會有賬號、密碼、驗證碼這三個信息,請求類代碼如下所示。

public class UserLoginCommand : IRequest<bool>
{
    /// <summary>
    /// 賬戶
    /// </summary>
    public string Account { get; private set; }

    /// <summary>
    /// 密碼
    /// </summary>
    public string Password { get; private set; }

    /// <summary>
    /// 驗證碼
    /// </summary>
    public string VerificationCode { get; private set; }

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="account">賬戶</param>
    /// <param name="password">密碼</param>
    /// <param name="verificationCode">驗證碼</param>
    public UserLoginCommand(string account, string password, string verificationCode)
    {
        Account = account;
        Password = password;
        VerificationCode = verificationCode;
    }
}

  當我們擁有了存儲用戶登錄請求信息的類之後,我們就需要對用戶的登錄請求進行處理。這裏,我們在 Sample.Application 這個類庫下面新建一個 CommandHandlers 文件夾用來存放用戶請求的處理類。

  現在我們創建一個繼承於 IRequestHandler 接口的 UserLoginCommandHandler 類用來實現對於用戶登錄請求的處理。IRequestHandler 是一個泛型的接口,它需要我們在繼承時聲明我們需要實現的請求,以及該請求的返回信息。因此,對於 UserLoginCommand 這個請求來說,UserLoginCommandHandler 這個請求的處理類,最終需要繼承於 IRequestHandler<UserLoginCommand, bool>。

  就像上面提到的一樣,我們需要在這個請求的處理類中對用戶請求的信息進行處理,在 UserLoginCommandHandler 類中,我們應該在 Handle 方法中去執行我們的判斷邏輯,這裏我們會引用到倉儲來獲取用戶的相關信息。倉儲中的代碼這裏我就不展示了,最終我們實現后的代碼如下所示。

public class UserLoginCommandHandler : IRequestHandler<UserLoginCommand, bool>
{
    #region Initizalize

    /// <summary>
    /// 倉儲實例
    /// </summary>
    private readonly IUserRepository _userRepository;

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="userRepository"></param>
    public UserLoginCommandHandler(IUserRepository userRepository)
    {
        _userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
    }

    #endregion Initizalize

    /// <summary>
    /// Command Handler
    /// </summary>
    /// <param name="request"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<bool> Handle(UserLoginCommand request, CancellationToken cancellationToken)
    {
        // 1、判斷驗證碼是否正確
        if (string.IsNullOrEmpty(request.VerificationCode))
            return false;

        // 2、驗證登錄密碼是否正確
        var appUser = await _userRepository.GetAppUserInfo(request.Account.Trim(), request.Password.Trim());
        if (appUser == null)
            return false;

        return true;
    }
}

  當我們完成了對於請求的處理代碼后,就可以在 controller 中提供用戶訪問的入口。當然,因為我們需要採用依賴注入的方式去使用 MediatR,所以在使用之前,我們需要將請求的對應處理關係注入到依賴注入容器中。

  在通過依賴注入的方式使用 MediatR 時,我們需要將所有的事件(請求以及通知)注入到容器中,而 MediatR 則會自動尋找對應事件的處理類,除此之外,我們也需要將通過依賴注入使用到的 IMediator 接口的實現類注入到容器中。而在這個示例項目中,我們主要是在 Sample.Domain、Sample.Application 以及我們的 Web Api 項目中使用到了 MediatR,因此,我們需要將這三個項目中使用到 MediatR 的類全部注入到容器中。

  一個個的注入會比較的麻煩,所以這裏我還是採用對指定的程序集進行反射操作,去獲取需要加載的信息批量的進行注入操作,最終實現后的代碼如下。

public static IServiceCollection AddCustomMediatR(this IServiceCollection services, MediatorDescriptionOptions options)
{
    // 獲取 Startup 類的 type 類型
    var mediators = new List<Type> { options.StartupClassType };

    // IRequest<T> 接口的 type 類型
    var parentRequestType = typeof(IRequest<>);

    // INotification 接口的 type 類型
    var parentNotificationType = typeof(INotification);

    foreach (var item in options.Assembly)
    {
        var instances = Assembly.Load(item).GetTypes();

        foreach (var instance in instances)
        {
            // 判斷是否繼承了接口
            //
            var baseInterfaces = instance.GetInterfaces();
            if (baseInterfaces.Count() == 0 || !baseInterfaces.Any())
                continue;

            // 判斷是否繼承了 IRequest<T> 接口
            //
            var requestTypes = baseInterfaces.Where(i => i.IsGenericType
                && i.GetGenericTypeDefinition() == parentRequestType);

            if (requestTypes.Count() != 0 || requestTypes.Any())
                mediators.Add(instance);

            // 判斷是否繼承了 INotification 接口
            //
            var notificationTypes = baseInterfaces.Where(i => i.FullName == parentNotificationType.FullName);

            if (notificationTypes.Count() != 0 || notificationTypes.Any())
                mediators.Add(instance);
        }
    }

    // 添加到依賴注入容器中
    services.AddMediatR(mediators.ToArray());

    return services;
}

  因為需要知道哪些程序集應該進行反射獲取信息,而對於 Web Api 這個項目來說,它只會通過依賴注入使用到 IMediator 這一個接口,所以這裏需要採用不同的參數的形式去確定具體需要通過反射加載哪些程序集。

public class MediatorDescriptionOptions
{
    /// <summary>
    /// Startup 類的 type 類型
    /// </summary>
    public Type StartupClassType { get; set; }

    /// <summary>
    /// 包含使用到 MediatR 組件的程序集
    /// </summary>
    public IEnumerable<string> Assembly { get; set; }
}

  最終,我們就可以在 Startup 類中通過擴展方法的信息進行快速的注入,實際使用的代碼如下,這裏我是將需要加載的程序集信息放在 appsetting 這個配置文件中的,你可以根據你的喜好進行調整。

public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        // Config mediatr
        services.AddCustomMediatR(new MediatorDescriptionOptions
        {
            StartupClassType = typeof(Startup),
            Assembly = Configuration["Assembly:Mediator"].Split("|", StringSplitOptions.RemoveEmptyEntries)
        });
    }
}

  在這個示例項目中的配置信息如下所示。

{
  "Assembly": {
    "Function": "Sample.Domain",
    "Mapper": "Sample.Application",
    "Mediator": "Sample.Application|Sample.Domain"
  }
}

  當我們注入完成后,就可以直接在 controller 中進行使用。對於繼承了 IRequest 的方法,可以直接通過 Send 方法進行調用請求信息,MediatR 會幫我們找到對應請求的處理方法,最終登錄 action 中的代碼如下。

[ApiVersion("1.0")]
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class UsersController : ControllerBase
{
    #region Initizalize

    /// <summary>
    ///
    /// </summary>
    private readonly IMediator _mediator;

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="mediator"></param>
    public UsersController(IMediator mediator)
    {
        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
    }

    #endregion Initizalize

    #region APIs

    /// <summary>
    /// 用戶登錄
    /// </summary>
    /// <param name="login">用戶登錄數據傳輸對象</param>
    /// <returns></returns>
    [HttpPost("login")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<IActionResult> Post([FromBody] AppUserLoginDto login)
    {
        // 實體映射轉換
        var command = new UserLoginCommand(login.Account, login.Password, login.VerificationCode);

        bool flag = await _mediator.Send(command);

        if (flag)
            return Ok(new
            {
                code = 20001,
                msg = $"{login.Account} 用戶登錄成功",
                data = login
            });
        else
            return Unauthorized(new
            {
                code = 40101,
                msg = $"{login.Account} 用戶登錄失敗",
                data = login
            });
    }

    #endregion APIs
}

  當我們完成了對於用戶登錄請求的處理之後,就可以去執行後續的“通知類”的事件。與用戶登錄的請求信息類相似,對於用戶登錄事件的通知類也只是包含一些通知的基礎信息。在 Smaple.Domain 這個類庫下面,創建一個 Events 文件用來存放我們的事件,我們來新建一個繼承於 INotification 接口的 AppUserLoginEvent 類,用來對用戶登錄事件進行相關的處理。

public class AppUserLoginEvent : INotification
{
    /// <summary>
    /// 賬戶
    /// </summary>
    public string Account { get; }

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="account"></param>
    public AppUserLoginEvent(string account)
    {
        Account = account;
    }
}

  在上文中有提到過,對於一個通知事件可能會存在着多種處理方式,所以這裏我們在 Smaple.Application 這個類庫的 DomainEventHandlers 文件夾下面會按照事件去創建對應的文件夾去存放實際處理方法。

  對於繼承了 INotification 接口的通知類來說,在 MediatR 中我們可以通過創建繼承於 INotificationHandler 接口的類去處理對應的事件。因為一個 notification 可以有多個的處理程序,所以我們可以創建多個的 NotificationHandler 類去處理同一個 notification。一個示例的 NotificationHandler 類如下所示。

public class SetCurrentUserEventHandler : INotificationHandler<AppUserLoginEvent>
{
    #region Initizalize

    /// <summary>
    ///
    /// </summary>
    private readonly ILogger<SetCurrentUserEventHandler> _logger;

    /// <summary>
    ///
    /// </summary>
    /// <param name="logger"></param>
    public SetCurrentUserEventHandler(ILogger<SetCurrentUserEventHandler> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    #endregion Initizalize

    /// <summary>
    /// Notification handler
    /// </summary>
    /// <param name="notification"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task Handle(AppUserLoginEvent notification, CancellationToken cancellationToken)
    {
        _logger.LogInformation($"CurrentUser with Account: {notification.Account} has been successfully setup");

        return Task.FromResult(true);
    }
}

  如何去引發這個事件,對於領域驅動設計的架構來說,一個更好的方法是將各種領域事件添加到事件的集合中,然後在提交事務之前或之後立即調度這些域事件,而對於我們這個項目來說,因為這不在這篇文章考慮的範圍內,只是演示如何去使用 MediatR 這個組件,所以這裏我就採取在請求邏輯處理完成后直接觸發事件的方式。

  在 UserLoginCommandHandler 類中,修改我們的代碼,在確認登錄成功后,通過調用 AppUser 類的 SetUserLoginRecord 方法來觸發我們的通知事件,修改后的代碼如下所示。

public class UserLoginCommandHandler : IRequestHandler<UserLoginCommand, bool>
{
    #region Initizalize

    /// <summary>
    /// 倉儲實例
    /// </summary>
    private readonly IUserRepository _userRepository;

    /// <summary>
    ///
    /// </summary>
    private readonly IMediator _mediator;

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="userRepository"></param>
    /// <param name="mediator"></param>
    public UserLoginCommandHandler(IUserRepository userRepository, IMediator mediator)
    {
        _userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
    }

    #endregion Initizalize

    /// <summary>
    /// Command Handler
    /// </summary>
    /// <param name="request"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<bool> Handle(UserLoginCommand request, CancellationToken cancellationToken)
    {
        // 1、判斷驗證碼是否正確
        if (string.IsNullOrEmpty(request.VerificationCode))
            return false;

        // 2、驗證登錄密碼是否正確
        var appUser = await _userRepository.GetAppUserInfo(request.Account.Trim(), request.Password.Trim());
        if (appUser == null)
            return false;

        // 3、觸發登錄事件
        appUser.SetUserLoginRecord(_mediator);

        return true;
    }
}

  與使用 Send 方法去調用 request 類的請求不同,對於繼承於 INotification 接口的事件通知類,我們需要採用 Publish 的方法去調用。至此,對於一個採用中介者模式設計的登錄流程就結束了,SetUserLoginRecord 方法的定義,以及最終我們實現的效果如下所示。

public void SetUserLoginRecord(IMediator mediator)
{
    mediator.Publish(new AppUserLoginEvent(Account));
}

 三、總結

  這一章主要是介紹了如何通過 MediatR 來實現中介者模式,因為自己也是第一次接觸這種思想,對於 MediatR 這個組件也是第一次使用,所以僅僅是採用案例分享的方式對中介者模式的使用方法進行了一個解釋。如果你想要對中介者模式的具體定義與基礎的概念進行進一步的了解的話,可能需要你自己去找資料去弄明白具體的定義。因為初次接觸,難免會有遺漏或錯誤,如果從文章中發現有不對的地方,歡迎在評論區中指出,先行感謝。

 四、參考

  1、

  2、

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

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

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

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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

※專營大陸快遞台灣服務

台灣快遞大陸的貨運公司有哪些呢?

分類
發燒車訊

50行Python代碼實現視頻中物體顏色識別和跟蹤(必須以紅色為例)

目前計算機視覺(CV)與自然語言處理(NLP)及語音識別並列為人工智能三大熱點方向,而計算機視覺中的對象檢測(objectdetection)應用非常廣泛,比如自動駕駛、視頻監控、工業質檢、醫療診斷等場景。

目標檢測的根本任務就是將圖片或者視頻中感興趣的目標提取出來,目標的識別可以基於顏色、紋理、形狀。其中顏色屬性運用十分廣泛,也比較容易實現。下面就向大家分享一個我做的小實驗———通過OpenCV的Python接口來實現從視頻中進行顏色識別和跟蹤。

下面就是我們完整的代碼實現(已調試運行):

import numpy as np
import cv2
font = cv2.FONT_HERSHEY_SIMPLEX
lower_green = np.array([35, 110, 106])  # 綠色範圍低閾值
upper_green = np.array([77, 255, 255])  # 綠色範圍高閾值
lower_red = np.array([0, 127, 128])  # 紅色範圍低閾值
upper_red = np.array([10, 255, 255])  # 紅色範圍高閾值
#需要更多顏色,可以去百度一下HSV閾值!
# cap = cv2.VideoCapture('1.mp4')  # 打開視頻文件
cap = cv2.VideoCapture(0)#打開USB攝像頭
if (cap.isOpened()):  # 視頻打開成功
    flag = 1
else:
    flag = 0
num = 0
if (flag):
    while (True):
        ret, frame = cap.read()  # 讀取一幀
       
        if ret == False:  # 讀取幀失敗
            break
        hsv_img = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        mask_green = cv2.inRange(hsv_img, lower_green, upper_green)  # 根據顏色範圍刪選
        mask_red = cv2.inRange(hsv_img, lower_red, upper_red) 
 # 根據顏色範圍刪選
        mask_green = cv2.medianBlur(mask_green, 7)  # 中值濾波
        mask_red = cv2.medianBlur(mask_red, 7)  # 中值濾波
        mask = cv2.bitwise_or(mask_green, mask_red)
        mask_green, contours, hierarchy = cv2.findContours(mask_green, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
        mask_red, contours2, hierarchy2 = cv2.findContours(mask_red, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

        for cnt in contours:
            (x, y, w, h) = cv2.boundingRect(cnt)
            cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 255), 2)
            cv2.putText(frame, "Green", (x, y - 5), font, 0.7, (0, 255, 0), 2)

        for cnt2 in contours2:
            (x2, y2, w2, h2) = cv2.boundingRect(cnt2)
            cv2.rectangle(frame, (x2, y2), (x2 + w2, y2 + h2), (0, 255, 255), 2)
            cv2.putText(frame, "Red", (x2, y2 - 5), font, 0.7, (0, 0, 255), 2)
        num = num + 1
        cv2.imshow("dection", frame)
        cv2.imwrite("imgs/%d.jpg"%num, frame)
        if cv2.waitKey(20) & 0xFF == 27:
            break
cv2.waitKey(0)
cv2.destroyAllWindows()

如圖所示,我們將會檢測到紅色區域

最終的效果圖:

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

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

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

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

台灣海運大陸貨務運送流程

兩岸物流進出口一站式服務

分類
發燒車訊

Zabbix-(四)郵件、釘釘告警通知

Zabbix-(四)郵件、釘釘告警通知

一.前言

在之前的文章里,通過Zabbix對主機的磁盤、CPU以及內存進行了監控,並在首頁Dashboard里創建了監控圖形,但是只有當我們登錄到Zabbix后才能看到監控到的問題(Problem),因此在本篇文章里,將利用觸發器(Trigger),以及媒介(Media)等配置項,實現當觸發器觸發時,通過不同媒介,如:郵件、釘釘,發送動作(Action),實現實時通知告警功能。

準備

  • Zabbix Server (Zabbix 4.4)
  • 在Zabbix中已配置一些監控項和觸發器(這些配置可以參考我的)

二.安裝相關環境

由於使用到腳本告警媒介,本文中通過調用Python腳本觸發告警,因此需要在Zabbix Server主機上安裝pip以及相關模塊。(這裏Python使用Centos7自帶的Python2.7.5)

  1. 安裝pip

    # yum install -y epel-release
    
    # yum install -y python-pip
  2. 安裝requests模塊

    # pip install requests

三.配置告警媒介類型

Zabbix默認自帶了2種報警媒介類型(Media Type)电子郵件以及短信,我們將修改电子郵件類型配置,並新建腳本類型和Webhook類型。希望通過腳本、Webhook告警媒介發送釘釘消息。

注:Webhook告警媒介是Zabbix 4.4的新特性

  1. 修改电子郵件告警媒介

    點擊【管理】-【報警媒介類型】-【Email】

    修改Email配置,我這裏用的是Outlook郵箱,具體SMTP服務器可以參考。使用其他郵箱也可以去對應官網查詢SMTP配置。

    測試發送郵箱,點擊【測試】

    輸入收件人郵箱

    收到郵件

  2. 新增腳本告警媒介

    新建Python腳本告警媒介,用戶釘釘告警

    點擊【創建媒體類型】

    進行配置

    配置項
    * 名稱 Python腳本
    類型 腳本
    * 腳本名稱 pythonScript.py
    腳本參數(參數1) {ALERT.MESSAGE}
    腳本參數(參數2) {ALERT.SENDTO}
    腳本參數(參數3) {ALERT.SUBJECT}

    接下來新建Python腳本,Zabbix Server配置文件中可以配置告警腳本路徑,默認為 /usr/lib/zabbix/alertscripts

    # 查看告警腳本路徑
    # cat zabbix_server.conf | grep AlertScriptsPath

    編寫告警腳本

    # cd /usr/lib/zabbix/alertscripts
    # vim pythonScript.py

    腳本內容

    #!/usr/bin/env python
    #coding:utf-8
    
    import requests,json,sys,os,datetime
    
    # 釘釘機器人地址
    webhook="https://oapi.dingtalk.com/robot/send?access_token=your_dingding_robot_access_token"
    
    # 對應{ALERT.SENDTO}, Zabbix告警媒介配置界面第2個參數
    user=sys.argv[2]
    
    # 對應{ALERT.MESSAGE}, Zabbix告警媒介配置界面第1個參數
    text=sys.argv[1]
    data={
        "msgtype": "text",
        "text": {
            "content": text
        },
        "at": {
            "atMobiles": [
                user
            ],
            "isAtAll": False
        }
    }
    headers = {'Content-Type': 'application/json'}
    x=requests.post(url=webhook,data=json.dumps(data),headers=headers)

    給腳本可執行權限

    # chmod uo+x /usr/lib/zabbix/alertscripts/pythonScript.py

    測試腳本

    釘釘收到消息

  3. 新增Webhook告警媒介

    配置項
    * 名稱 Webhook
    類型 Webhook
    參數: (名稱)
    user {ALERT.SENDTO}
    subject {ALERT.SUBJECT}
    message {ALERT.MESSAGE}

    腳本:

    try {
        Zabbix.Log(4, 'params= '+value);
    
        params = JSON.parse(value);
        req = new CurlHttpRequest();
        data = {};
        result = {};
    
        req.AddHeader('Content-Type: application/json');
    
        data.msgtype = "text";
        //   對應 message參數
        data.text = {"content" : params.message};
        //   對應 user參數
        data.at = {"atMobiles": [params.user], "isAtAll": "false"};
    
        //   釘釘機器人
        resp = req.Post('https://oapi.dingtalk.com/robot/send?access_token=your_access_token',
            JSON.stringify(data)
        );
    } catch (error) {
        result = {};
    }
    
    return JSON.stringify(result);

    測試Webhook

四.為用戶添加告警媒介

需要將新增的告警媒介添加給用戶

點擊【用戶】-【告警媒介】

將上述步驟添加的告警媒介(Python腳本、Webhoob、Email),進行添加(收件人根據告警媒介類型填寫郵箱手機號),嚴重性也根據需要勾選。

五.配置動作

完成上述配置完成后,需要創建動作(Action),將觸發器(Trigger)告警媒介(Media Type)進行關聯,一旦觸發器觸發,那麼Zabbix會執行動作,再去執行告警媒介。

  1. 添加動作

    點擊【配置】-【動作】-【創建動作】

  2. 配置【動作】相關信息

    配置項
    * 名稱 告警動作
    新的觸發條件 【觸發器】【等於】【Template Disk Free Size: 磁盤剩餘空間觸發器】

    操作步驟如下圖:

    群組選擇 ->Linux servers

    主機選擇 -> Template Disk Free Size 模板()

    勾選觸發器 -> 磁盤剩餘空間觸發器 ()

    勾選後點擊【選擇】

  3. 配置【操作】相關信息

    點擊【操作】

    先配置以下信息

    配置項
    * 默認操作步驟持續時間 1h(保持默認)
    默認標題 告警: {EVENT.NAME}
    消息內容 【磁盤空間不足告警】
    告警事件: {EVENT.DATE} {EVENT.TIME}
    告警問題: {EVENT.NAME}
    告警主機: {HOST.IP} {HOST.NAME}
    告警級別: {EVENT.SEVERITY}
    磁盤剩餘:{ITEM.VALUE}

    上述配置表格【默認標題】和【消息內容】值中形如{EVENT.NAME}的內容是Zabbix中的宏(Marco),宏是一個變量,例如 {HOST.IP} 表示告警主機的IP地址,Zabbix自帶的宏可以參考

    繼續配置操作

    點擊【新的】

    【操作類型】選擇發送消息,【發送到用戶】添加Admin

    【僅送到】根據需要選擇之前配置的,本文選擇Email和Python腳本(這裏只能單選或全選,所以需要先選擇一個,因此需要多次)

    添加完成後點擊【添加】

六.測試

向被監控主機拷貝或下載大文件,使其磁盤剩餘空間低於觸發器監控閾值,等待觸發器觸發問題,查看儀錶盤、郵件等。

儀錶盤

釘釘

郵件

七.參考文檔

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

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包”嚨底家”

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

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

小三通海運與一般國際貿易有何不同?

小三通快遞通關作業有哪些?

分類
發燒車訊

Dev 日誌 | 一次 Segmentation Fault 和 GCC Illegal Instruction 編譯問題排查

摘要

筆者最近在重新整理和編譯 Nebula Graph 的第三方依賴,選出兩個比較有意思的問題給大家分享一下。

Flex Segmentation Fault——Segmentation fault (core dumped)

在編譯 Flex 過程中,遇到了 Segmentation fault:

make[2]: Entering directory '/home/dutor/flex-2.6.4/src'
./stage1flex   -o stage1scan.c ./scan.l
make[2]: *** [Makefile:1696: stage1scan.c] Segmentation fault (core dumped)

使用 gdb 查看 coredump:

Core was generated by `./stage1flex -o stage1scan.c ./scan.l'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  flexinit (argc=4, argv=0x7ffd25bea718) at main.c:976
976             action_array[0] = '\0';
(gdb) disas
Dump of assembler code for function flexinit:
   0x0000556c1b1ae040 <+0>:     push   %r15
   0x0000556c1b1ae042 <+2>:     lea    0x140fd(%rip),%rax        # 0x556c1b1c2146
   ...
   0x0000556c1b1ae20f <+463>:   callq  0x556c1b1af460 <allocate_array> #這裏申請了buffer
   ...
=> 0x0000556c1b1ae24f <+527>:   movb   $0x0,(%rax) # 這裏向buffer[0]寫入一個字節,地址非法,掛掉了
   ...
(gdb) disas allocate_array
Dump of assembler code for function allocate_array:
   0x0000556c1b1af460 <+0>:     sub    $0x8,%rsp
   0x0000556c1b1af464 <+4>:     mov    %rsi,%rdx
   0x0000556c1b1af467 <+7>:     xor    %eax,%eax
   0x0000556c1b1af469 <+9>:     movslq %edi,%rsi
   0x0000556c1b1af46c <+12>:    xor    %edi,%edi
   0x0000556c1b1af46e <+14>:    callq  0x556c1b19a100 <reallocarray@plt> # 調用庫函數申請內存
   0x0000556c1b1af473 <+19>:    test   %eax,%eax # 判斷是否為 NULL
   0x0000556c1b1af475 <+21>:    je     0x556c1b1af47e <allocate_array+30># 跳轉至NULL錯誤處理
   0x0000556c1b1af477 <+23>:    cltq   # 將 eax 符號擴展至 rax,造成截斷
   0x0000556c1b1af479 <+25>:    add    $0x8,%rsp
   0x0000556c1b1af47d <+29>:    retq
   ...
End of assembler dump.

可以看到,問題出在了 allocate_array 函數。因為 reallocarray 返回指針,返回值應該使用 64 bit 寄存器rax,但 allocate_array 調用 reallocarray 之後,檢查的卻是 32 bit 的 eax,同時使用 cltq 指令將 eax 符號擴展 到 rax。原因只有一個:allocate_array 看到的 reallocarray 的原型,與 reallocarry 的實際定義不符。翻看編譯日誌,確實找到了 implicit declaration of function ‘reallocarray’ 相關的警告。configure 階段添加 CFLAGS=-D_GNU_SOURCE 即可解決此問題。

注:此問題不是必現,但編譯/鏈接選項 -pie 和 內核參數 kernel.randomize_va_space 有助於復現。

總結:

  • 隱式聲明的函數在 C 中,返回值被認為是 int
  • 關注編譯器告警,-Wall -Wextra 要打開,開發模式下最好打開 -Werror。

GCC Illegal Instruction——internal compiler error: Illegal instruction

前陣子,接到用戶反饋,在編譯 Nebula Graph 過程中遭遇了編譯器非法指令的錯誤,詳見(#978)[]

錯誤信息大概是這樣的:

Scanning dependencies of target base_obj_gch
[ 0%] Generating Base.h.gch
In file included from /opt/nebula/gcc/include/c++/8.2.0/chrono:40,
from /opt/nebula/gcc/include/c++/8.2.0/thread:38,
from /home/zkzy/nebula/nebula/src/common/base/Base.h:15:
/opt/nebula/gcc/include/c++/8.2.0/limits:1599:7: internal compiler error: Illegal instruction
min() _GLIBCXX_USE_NOEXCEPT { return FLT_MIN; }
^~~
0xb48c5f crash_signal
../.././gcc/toplev.c:325
Please submit a full bug report,
with preprocessed source if appropriate.

既然是 internal compiler error,想必是 g++ 本身使用了非法指令。為了定位具體的非法指令集及其所屬模塊,我們需要復現這個問題。幸運的是,下面的代碼片段就能觸發:

#include <thread>
int main() 
{
    return 0;
}

非法指令一定會觸發 SIGILL,又因為 g++ 只是編譯器的入口,真正幹活的是 cc1plus。我們可以使用 gdb 來運行編譯命令,抓住子進程使用非法指令的第一現場:

$ gdb --args /opt/nebula/gcc/bin/g++ test.cpp
gdb> set follow-fork-mode child
gdb> run
Starting program: /opt/nebula/gcc/bin/g++ test.cpp
[New process 31172]
process 31172 is executing new program: /opt/nebula/gcc/libexec/gcc/x86_64-pc-linux-gnu/8.2.0/cc1plus
Thread 2.1 "cc1plus" received signal SIGILL, Illegal instruction.
[Switching to process 31172]
0x00000000013aa0fb in __gmpn_mul_1 ()
gdb> disas
...
0x00000000013aa086 <+38>: mulx (%rsi),%r10,%r8
...

Bingo!mulx 屬於 BMI2 指令集,報錯機器 CPU 不支持該指令集。
仔細調查,引入該指令集的是 GCC 的依賴之一,GMP。默認情況下,GMP 會在 configure 階段探測當前機器的 CPU 具體類型,以期最大化利用 CPU 的擴展指令集,提升性能,但卻犧牲了二進制的可移植性。解決方法是,configure 之前,使用代碼目錄中的 configfsf.guess configfsf.sub 替換或者覆蓋默認的 config.guess 和 config.sub

總結:

  • 某些依賴可能因為性能或者配置的原因,造成二進制的不兼容。
  • 缺省參數下,GCC 為了兼容性,不會使用較新的指令集。
  • 為了平衡兼容性和性能,你需要做一些額外的工作,比如像 glibc 那樣在運行時選擇和綁定某個具體實現。

最後,如果你想嘗試編譯一下 Nebula 源代碼可參考以下方式:

bash> git clone https://github.com/vesoft-inc/nebula.git
bash> cd nebula && ./build_dep.sh N

有問題請在 GitHub 或者微信公眾號上留言。

附錄

  • Nebula Graph:一個開源的分佈式圖數據庫
  • GitHub:
  • 知乎:
  • 微博:

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

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

小三通物流營運型態?

※快速運回,大陸空運推薦?

分類
發燒車訊

Feign 調用丟失Header的解決方案

問題

在 Spring Cloud 中 微服務之間的調用會用到Feign,但是在默認情況下,Feign 調用遠程服務存在Header請求頭丟失問題。

解決方案

首先需要寫一個 Feign請求攔截器,通過實現RequestInterceptor接口,完成對所有的Feign請求,傳遞請求頭和請求參數。

Feign 請求攔截器

public class FeignBasicAuthRequestInterceptor implements RequestInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(FeignBasicAuthRequestInterceptor.class);

    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Enumeration<String> headerNames = request.getHeaderNames();
        if (headerNames != null) {
            while (headerNames.hasMoreElements()) {
                String name = headerNames.nextElement();
                String values = request.getHeader(name);
                requestTemplate.header(name, values);
            }
        }
        Enumeration<String> bodyNames = request.getParameterNames();
        StringBuffer body =new StringBuffer();
        if (bodyNames != null) {
            while (bodyNames.hasMoreElements()) {
                String name = bodyNames.nextElement();
                String values = request.getParameter(name);
                body.append(name).append("=").append(values).append("&");
            }
        }
        if(body.length()!=0) {
            body.deleteCharAt(body.length()-1);
            requestTemplate.body(body.toString());
            logger.info("feign interceptor body:{}",body.toString());
        }
    }
}

通過配置文件配置 讓 所有 FeignClient,來使用 FeignBasicAuthRequestInterceptor

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic
        requestInterceptors: com.leparts.config.FeignBasicAuthRequestInterceptor

也可以配置讓 某個 FeignClient 來使用這個 FeignBasicAuthRequestInterceptor

feign:
  client:
    config:
      xxxx: # 遠程服務名
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic
        requestInterceptors: com.leparts.config.FeignBasicAuthRequestInterceptor

經過測試,上面的解決方案可以正常的使用;但是出現了新的問題。

在轉發Feign的請求頭的時候, 如果開啟了Hystrix, Hystrix的默認隔離策略是Thread(線程隔離策略), 因此轉發攔截器內是無法獲取到請求的請求頭信息的。

可以修改默認隔離策略為信號量模式:

hystrix.command.default.execution.isolation.strategy=SEMAPHORE

但信號量模式不是官方推薦的隔離策略;另一個解決方法就是自定義Hystrix的隔離策略。

自定義策略

HystrixConcurrencyStrategy 是提供給開發者去自定義hystrix內部線程池及其隊列,還提供了包裝callable的方法,以及傳遞上下文變量的方法。所以可以繼承了HystrixConcurrencyStrategy,用來實現了自己的併發策略。

@Component
public class FeignHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {

    private static final Logger log = LoggerFactory.getLogger(FeignHystrixConcurrencyStrategy.class);

    private HystrixConcurrencyStrategy delegate;

    public FeignHystrixConcurrencyStrategy() {
        try {
            this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy();
            if (this.delegate instanceof FeignHystrixConcurrencyStrategy) {
                // Welcome to singleton hell...
                return;
            }

            HystrixCommandExecutionHook commandExecutionHook =
                    HystrixPlugins.getInstance().getCommandExecutionHook();

            HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier();
            HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher();
            HystrixPropertiesStrategy propertiesStrategy =
                    HystrixPlugins.getInstance().getPropertiesStrategy();
            this.logCurrentStateOfHystrixPlugins(eventNotifier, metricsPublisher, propertiesStrategy);

            HystrixPlugins.reset();
            HystrixPlugins instance = HystrixPlugins.getInstance();
            instance.registerConcurrencyStrategy(this);
            instance.registerCommandExecutionHook(commandExecutionHook);
            instance.registerEventNotifier(eventNotifier);
            instance.registerMetricsPublisher(metricsPublisher);
            instance.registerPropertiesStrategy(propertiesStrategy);
        } catch (Exception e) {
            log.error("Failed to register Sleuth Hystrix Concurrency Strategy", e);
        }
    }

    private void logCurrentStateOfHystrixPlugins(HystrixEventNotifier eventNotifier,
                                                 HystrixMetricsPublisher metricsPublisher,
                                                 HystrixPropertiesStrategy propertiesStrategy) {
        if (log.isDebugEnabled()) {
            log.debug("Current Hystrix plugins configuration is [" + "concurrencyStrategy ["
                    + this.delegate + "]," + "eventNotifier [" + eventNotifier + "]," + "metricPublisher ["
                    + metricsPublisher + "]," + "propertiesStrategy [" + propertiesStrategy + "]," + "]");
            log.debug("Registering Sleuth Hystrix Concurrency Strategy.");
        }
    }

    @Override
    public <T> Callable<T> wrapCallable(Callable<T> callable) {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        return new WrappedCallable<>(callable, requestAttributes);
    }

    @Override
    public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
                                            HystrixProperty<Integer> corePoolSize,
                                            HystrixProperty<Integer> maximumPoolSize,
                                            HystrixProperty<Integer> keepAliveTime,
                                            TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        return this.delegate.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime,
                unit, workQueue);
    }

    @Override
    public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
                                            HystrixThreadPoolProperties threadPoolProperties) {
        return this.delegate.getThreadPool(threadPoolKey, threadPoolProperties);
    }

    @Override
    public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) {
        return this.delegate.getBlockingQueue(maxQueueSize);
    }

    @Override
    public <T> HystrixRequestVariable<T> getRequestVariable(HystrixRequestVariableLifecycle<T> rv) {
        return this.delegate.getRequestVariable(rv);
    }

    static class WrappedCallable<T> implements Callable<T> {
        private final Callable<T> target;
        private final RequestAttributes requestAttributes;

        WrappedCallable(Callable<T> target, RequestAttributes requestAttributes) {
            this.target = target;
            this.requestAttributes = requestAttributes;
        }

        @Override
        public T call() throws Exception {
            try {
                RequestContextHolder.setRequestAttributes(requestAttributes);
                return target.call();
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        }
    }
}

致此,Feign調用丟失請求頭的問題就解決的了 。

參考

https://blog.csdn.net/zl1zl2zl3/article/details/79084368
https://cloud.spring.io/spring-cloud-static/spring-cloud-openfeign/2.2.0.RC2/reference/html/

歡迎掃碼或微信搜索公眾號《程序員果果》關注我,關注有驚喜~

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

【其他文章推薦】

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

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

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

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

台中搬家,彰化搬家,南投搬家前需注意的眉眉角角,別等搬了再說!

分類
發燒車訊

Kafka needs no Keeper(關於KIP-500的討論)

寫在前面的

最近看了Kafka Summit上的這個分享,覺得名字很霸氣,標題直接沿用了。這個分享源於社區的,大體的意思今後Apache Kafka不再需要ZooKeeper。整個分享大約40幾分鐘。完整看下來感覺乾貨很多,這裏特意總結出來。如果你把這個分享看做是《三國志》的話,那麼姑且就把我的這篇看做是裴松之注吧:)

客戶端演進

首先,社區committer給出了Kafka Java客戶端移除ZooKeeper依賴的演進過程。下面兩張圖總結了0.8.x版本和0.11.x版本(是否真的是從0.11版本開始的變化並不重要)及以後的功能變遷:在Kafka 0.8時代,Kafka有3個客戶端,分別是Producer、Consumer和Admin Tool。其中Producer負責向Kafka寫消息,Consumer負責從Kafka讀消息,而Admin Tool執行各種運維任務,比如創建或刪除主題等。其中Consumer的位移數據保存在ZooKeeper上,因此Consumer端的位移提交和位移獲取操作都需要訪問ZooKeeper。另外Admin Tool執行運維操作也要訪問ZooKeeper,比如在對應的ZooKeeper znode上創建一個臨時節點,然後由預定義的Watch觸發相應的處理邏輯。

後面隨着Kafka的演進,社區引入了__consumer_offsets位移主題,同時定義了OffsetFetch和OffsetCommit等新的RPC協議,這樣Consumer的位移提交和位移獲取操作全部轉移到與位移主題進行交互,避免了對ZooKeeper的訪問。同時社區引入了新的運維工具AdminClient以及相應的CreateTopics、DeleteTopics、AlterConfigs等RPC協議,替換了原先的Admin Tool,這樣創建和刪除主題這樣的運維操作也完全移動Kafka這一端來做,就像下面右邊這張圖展示的:

至此, Kafka的3個客戶端基本上都不需要和ZooKeeper交互了。應該說移除ZooKeeper的工作完成了大部分,但依然還有一部分工作要在ZooKeeper的幫助下完成,即Consumer的Rebalance操作。在0.8時代,Consumer Group的管理是交由ZooKeeper完成的,包括組成員的管理和訂閱分區的分配。這個設計在新版Consumer中也得到了修正。全部的Group管理操作交由Kafka Broker端新引入的Coordinator組件來完成。要完成這些工作,Broker端新增了很多RPC協議,比如JoinGroup、SyncGroup、Heartbeat、LeaveGroup等。

  

此時,Kafka的Java客戶端除了AdminClient還有一點要依賴ZooKeeper之外,所有其他的組件全部擺脫了對ZooKeeper的依賴。

之後,社區引入了Kafka安全層,實現了對用戶的認證和授權。這個額外的安全層也是不需要訪問ZooKeeper的,因此之前依賴ZooKeeper的客戶端是無法“享用”這個安全層。一旦啟用,新版Clients都需要首先接入這一層並通過審核之後才能訪問到Broker,如下圖所示:

這麼做的好處在於統一了Clients訪問Broker的模式,即定義RPC協議,比如我們熟知的PRODUCE協議、FETCH協議、METADATA協議、CreateTopics協議等。如果後面需要實現更多的功能,社區只需要定義新的RPC協議即可。同時新引入的安全層負責對這套RPC協議進行安全校驗,統一了訪問模式。另外這些協議都是版本化的(versioned),因此能夠獨立地進行演進,同時也兼顧了兼容性方面的考量。

Broker間交互

說完了Clients端,我們說下Broker端的現狀。目前,應該說Kafka Broker端對ZooKeeper是重度依賴的,主要表現在以下幾個方面:

  • Broker註冊管理
  • ACL安全層配置管理
  • 動態參數管理
  • 副本ISR管理
  • Controller選舉

我們拿一張圖來說明,圖中有4個Broker節點和一個ZooKeeper,左上角的Broker充當Controller的角色。當前,所有的Broker啟動后都必須維持與ZooKeeper的會話。Kafka依賴於這個會話實現Broker端的註冊,而且Kafka集群中的所有配置信息、副本信息、主題信息也都保存在ZooKeeper上。最後Controller與集群中每個Broker都維持了一個TCP長連接用於向這些Broker發送RPC請求。當前的Controller RPC類型主要有3大類:

  • LeaderAndIsr:主要用於向集群廣播主題分區Leader和ISR的變更情況,比如對應的Broker應該是特定分區的Leader還是Follower
  • StopReplica:向集群廣播執行停止副本的命令
  • UpdateMetadata:向集群廣播執行變更元數據信息的命令

圖中還新增了一個AlterISR RPC,這是KIP-497要實現的新RPC協議。現階段Kafka各個主題的ISR信息全部保存在ZooKeeper中。如果後續要捨棄ZooKeeper,必須要將這些信息從ZooKeeper中移出來,放在了Controller一端來做。同時還要在程序層面支持對ISR的管理。因此社區計劃在KIP-497上增加AlterISR協議。對了,還要提一句,當前Controller的選舉也是依靠ZooKeeper完成的。

所以後面Broker端的演進可能和Clients端的路線差不多:首先是把Broker與ZooKeeper的交互全部幹掉,只讓Controller與ZooKeeper進行交互,而其他所有Broker都只與Controller交互,如下圖所示:

 

看上去這種演進路線社區已經走得輕車熟路了,但實際上還有遺留了一些問題需要解決。

Broker Liveness

首先就是Broker的liveness問題,即Kafka如何判斷一個Broker到底是否存活?在目前的設計中,Broker的生存性監測完全依賴於與ZooKeeper之間的會話。一旦會話超時或斷開Controller自動觸發ZooKeeper端的Watch來移除該Broker,並對其上的分區做善後處理。如果移除了ZooKeeper,Kafka應該採用什麼機制來判斷Broker的生存性是一個問題。

Network Partition

如何防範網絡分區也是一個需要討論的話題。當前可能出現的Network Partition有4種:1、單個Broker完全與集群隔離;2、Broker間無法通訊;3、Broker與ZooKeeper無法通訊;4、Broker與Controller無法通訊。下面4張圖分別展示了這4種情況:

 

我們分別討論下。首先是第一種情況,單Broker與集群其他Broker隔離,這其實並不算太嚴重的問題。當前的設計已然能夠保證很好地應對此種情況。一旦Broker被隔離,Controller會將其從集群中摘除,雖然可用性降低了,但是整個集群的一致性依然能夠得到保證。第二種情況是Broker間無法通訊,可能的後果是消息的備份機制無法執行,Kafka要收縮ISR,依然是可用性上的降低,但是一致性狀態並沒有被破壞。情況三是Broker無法與ZooKeeper通訊。Broker能正常運轉,它只是無法與ZooKeeper進行通訊。此時我們說該Broker處於殭屍狀態,即所謂的Zoobie狀態。因Zoobie狀態引入的一致性bug社區jira中一直沒有斷過,社區這幾年也一直在修正這方面的問題,主要對抗的機制就是fencing。比如leader epoch等。最後一類情況是Broker無法與Controller通訊,那麼所有的元數據更新通道被堵死,即使這個Broker依然是healthy的,但是它保存的元數據信息可能是非常過期的。這樣連接該Broker的客戶端可能會看到各種非常古怪的問題。之前在知乎上回答過類似的問題:4。目前,社區對這種情況並沒有太好的解決辦法,主要的原因是Broker的liveness完全交由ZooKeeper來做的。一旦Broker與ZooKeeper之間的交互沒有問題,其他原因導致的liveness問題就無法徹底規避。

第四類Network Partition引入了一個經典的場景:元數據不一致。目前每個Broker都緩存了一份集群的元數據信息,這份數據是異步更新的。當第四類Partition發生時,Broker端緩存的元數據信息必然與Controller的不同步,從而造成各種各樣的問題。

下面簡要介紹一下元數據更新的過程。主要的流程就是Controller啟動時會同步地從ZooKeeper上拉取集群全量的元數據信息,之後再以異步的方式同步給其他Broker。其他Broker與Controller之間的同步往往有一個時間差,也就是說可能Clients訪問的元數據並不是最新的。我個人認為現在社區很多flaky test failure都是因為這個原因導致的。 事實上,實際使用過程中有很多場景是Broker端的元數據與Controller端永遠不同步。通常情況下如果我們不重啟Broker的話,那麼這個Broker上的元數據將永遠“錯誤”下去。好在社區還給出了一個最後的“大招”: 登錄到ZooKeeper SHELL,手動執行rmr /controller,強迫Controller重選舉,然後重新加載元數據,並給所有Broker重刷一份。不過在實際生產環境,我懷疑是否有人真的要這麼干,畢竟代價不小,而且最關鍵的是這麼做依然可能存在兩個問題:1. 我們如何確保Controller和Broker的數據是一致的?2. 加載元數據的過程通常很慢。

這裏詳細說說第二點,即加載元數據的性能問題。總體來說,加載元數據是一個O(N)時間複雜度的過程,這裏的N就是你集群中總的分區數。考慮到Controller從ZooKeeper加載之後還要推給其他的Broker,那麼做這件事的總的時間複雜度就是O(N * M),其中M是集群中Broker的數量。可以想見,當M和N都很大時,在集群中廣播元數據不是一個很快的過程。

Metadata as an Event Log

Okay,鑒於以上所提到的所有問題,當Kafka拋棄了ZooKeeper之後,社區應該如何解決它們呢?總體的思路就是Metadata as an Event Log + Controller quorum。我們先說metadata as an event log。如果你讀過Jay Kreps的《I ️Logs》,你應該有感觸,整個Kafka的架構其實都是構建在Log上的。每個topic的分區本質上就是一個Commit Log,但元數據信息的保存卻不是Log形式。在現有的架構設計中你基本上可以認為元數據的數據結構是KV形式的。這一次,社區採用了與消息相同的數據保存方式,即將元數據作為Log的方式保存起來,如下錶所示:

 

這樣做的好處在於每次元數據的變更都被當做是一條消息保存在Log中,而這個Log可以被視作是一個普通的Kafka主題被備份到多台Broker上。Log的一個好處在於它有清晰的前後順序關係,即每個事件發生的時間是可以排序的,配合以恰當的處理邏輯,我們就能保證對元數據變更的處理是按照變更發生時間順序處理,不出現亂序的情形。另外Log機制還有一個好處是,在Broker間同步元數據時,我們可以選擇同步增量數據(delta),而非全量狀態。現在Kafka Broker間同步元數據都是全量狀態同步的。前面說過了,當集群分區數很大時,這個開銷是很可觀的。如果我們能夠只同步增量狀態,勢必能極大地降低同步成本。最後一個好處是,我們可以很容易地量化元數據同步的進度,因為對Log的消費有位移數據,因此通過監控Log Lag就能算出當前同步的進度或是落後的進度。

採用Log機制后,其他Broker像是一個普通的Consumer,從Controller拉取元數據變更消息或事件。由於每個Broker都是一個Consumer,所以它們會維護自己的消費位移,就像下面這張圖一樣:

 這種設計下,Controller所在的Broker必須要承擔起所有元數據topic的管理工作,包括創建topic、管理topic分區的leader以及為每個元數據變更創建相應的事件等。既然社區選擇和__consumer_offsets類似的處理方式,一個很自然的問題在於這個元數據topic的管理是否能夠復用Kafka現有的副本機制?答案是:不可行。理由是現有的副本機制依賴於Controller,因此Kafka沒法依靠現有的副本機制來實現Controller——按照我們的俗語來說,這有點雞生蛋、蛋生雞的問題,屬於典型的循環依賴。為了實現這個,Kafka需要一套leader選舉協議,而這套協議或算法是不依賴於Controller的,即它是一個自管理的集群quorum(抱歉,在分佈式領域內,特別是分佈式共識算法領域中,針對quorum的恰當翻譯我目前還未找到,因此直接使用quorum原詞了)。最終社區決定採用Raft來實現這組quorum。這就是上面我們提到的第二個解決思路:Controller quorum。

Controller Quorum

與藉助Controller幫忙選擇Leader不同,Raft是讓自己的節點自行選擇Leader並最終令所有節點達成共識——對選擇Controller而言,這是一個很好的特性。其實Kafka現有的備份機制與Raft已經很接近了,下錶羅列了一下它們的異同:

 一眼掃過去,其實Kafka的備份機制和Raft很類似,比如Kafka中的offset其實就是Raft中的index,epoch對應於term。當然Raft中採用的半數機制來確保消息被提交以及Leader選舉,而Kafka設計了ISR機制來實現這兩點。總體來說,社區認為只需要對備份機製做一些小改動就應該可以很容易地切換到Raft-based算法。

下面這張圖展示Controller quorum可能更加直觀:

整個controller quorum類似於一個小的集群。和ZooKeeper類似,這個quorum通常是3台或5台機器,不需要讓Kafka中的每個Broker都自動稱為這個quorum中的一個節點。該quorum裏面有一個Leader負責處理客戶端發來的讀寫請求,這個Leader就是Kafka中的active controller。根據ZooKeeper的Zab協議,leader處理所有的寫請求,而follower是可以處理讀請求的。當寫請求發送給follower后,follower會將該請求轉發給leader處理。不過我猜Kafka應該不會這樣實現,它應該只會讓leader(即active controller)處理所有的讀寫請求,而客戶端(也就是其他Broker)壓根就不會發送讀寫請求給follower。在這一點上,這種設計和現有的Kafka請求處理機制是一致的。

現在還需要解決一個問題,即Leader是怎麼被選出來的?既然是Raft-based,那麼採用的也是Raft算法中的Leader選舉策略。讓Raft選出的Leader稱為active controller。網上有很多關於Raft選主的文章,這裏就不在贅述了,有興趣的可以讀一讀Raft的論文:《In Search of an Understandable Consensus Algorithm(Extended Version)》。

這套Raft quorum的一個好處在於它天然提供了低延時的failover,因此leader的切換會非常的迅速和及時,因為理論上不再有元數據加載的過程了,所有的元數據現在都同步保存follower節點的內存中,它已經有其他Broker需要拉取的所有元數據信息了!更酷的是,它避免了現在機制中一旦Controller切換要全量拉取元數據的低效行為,Broker無需重新拉取之前已經“消費”的元數據變更消息,它只需要從新Leader繼續“消費”即可。

另一個好處在於:採用了這套機制后,Kafka可以做元數據的緩存了(metadata caching):即Broker能夠把元數據保存在磁盤上,同時就像剛才說的,Broker只需讀取它關心的那部分數據即可。還有,和現在snapshot機制類似,如果一個Broker保存的元數據落後Controller太多或者是一個全新的Broker,Kafka甚至可以像Raft那樣直接發送一個snapshot文件,快速令其追上進度。當然大多數情況下,Broker只需要拉取delta增量數據即可。

Post KIP-500 Broker註冊

當前Broker啟動之後會向ZooKeeper註冊自己的信息,比如自己的主機名、端口、監聽協議等數據。移除ZooKeeper之後,Broker的註冊機制也要發生變化:Broker需要向active controller發送心跳來進行註冊。Controller收集心跳中包含的Broker數據構建整個Kafka集群信息,如下圖所示:

 同時Controller也會對心跳進行響應,顯式地告知Broker它們是否被允許加入集群——如果不允許,則可能需要被隔離(fenced)。當然controller自己也可以對自己進行隔離。我們針對前面提到的隔離場景討論下KIP-500是怎麼應對的。

Fencing

首先是普通Broker與集群完全隔離的場景,比如該Broker無法與controller和其他Broker進行通信,但它依然可以和客戶端程序交互。此時,fencing機制就很簡單了,直接讓controller令其下線即可。這和現在依靠ZooKeeper會話機制維持Broker判活的機制是一模一樣的,沒有太大改進。

第二種情況是Broker間的通訊中斷。此時消息無法在leader、follower間進行備份。但是對於元數據而言,我們不會看到數據不一致的情形,因為Broker依然可以和controller通訊,因此也不會有什麼問題。

第三種情況是Broker與Controller的隔離。現有機制下這是個問題,但KIP-500之後,Controller僅僅將該Broker“踢出場”即可,不會造成元數據的不一致。

最後一種情況是Broker與ZooKeeper的隔離, 既然ZooKeeper要被移除了,自然這也不是問題了。

部署

終於聊到KIP-500之後的Kafka運維了。下錶總結了KIP-500前後的部署情況對比:

很簡單,現在任何時候部署和運維Kafka都要考慮對ZooKeeper的運維管理。在KIP-500之後我們只需要關心Kafka即可。

Controller quorum共享模式

如前所述,controller改成Raft quorum機制后,可能使用3或5台機器構成一個小的quorum。那麼一個很自然的問題是,這些Broker機器還能否用作他用,是唯一用作controller quorum還是和其他Broker一樣正常處理。社區對此也做了解釋:兩種都支持!

如果你的Kafka集群資源很緊張,你可以使用共享controller模式(Shared Controller Mode),即充當controller quorum的Broker機器也能處理普通的客戶端請求;相反地,如果你的Kafka資源很充足,專屬controller模式(Separate Controller Mode)可能是更適合的,即在controller quorum中的Broker機器排它地用作Controller的選舉之用,不再對客戶端提供讀寫服務。這樣可以實現更好的資源隔離,適用於大集群。

Roadmap

最後說一下KIP-500的計劃。社區計劃分三步走:

第一步是移除客戶端對ZooKeeper的依賴——這一步基本上已經完成了,除了目前AdminClient還有少量的API依賴ZooKeeper之外,其他客戶端應該說都不需要訪問ZooKeeper了;第二步是移除Broker端的ZooKeeper依賴:這主要包括移除Broker端需要訪問ZooKeeper的代碼,以及增加新的Broker端API,如前面所說的AlterISR等,最後是將對ZooKeeper的訪問全部集中在controller端;最後一步就是實現controller quorum,實現Raft-based的quorum負責controller的選舉。

至於Kafka升級,如果從現有的Kafka直接升級到KIP-500之後的Kafka會比較困難,因此社區打算引入一個名為Bridge Release的中間過渡版本,如下圖所示:

這個Bridge版本的特點在於所有對ZooKeeper的訪問都集中到了controller端,Broker訪問ZooKeeper的其他代碼都被移除了。 

總結

KIP-500應該說是最近幾年社區提出的最重磅的KIP改進了。它幾乎是顛覆了Kafka已有的使用模式,摒棄了之前重度依賴的Apache ZooKeeper。就我個人而言,我是很期待這個KIP,後續有最新消息我也會在一併同步出來。讓我們靜觀其變吧~~~

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

【其他文章推薦】

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

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

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

大陸寄台灣空運注意事項

大陸海運台灣交貨時間多久?

※避免吃悶虧無故遭抬價!台中搬家公司免費估價,有契約讓您安心有保障!

分類
發燒車訊

前後端分離,我怎麼就選擇了 Spring Boot + Vue 技術棧?

前两天又有小夥伴私信松哥,問題還是職業規劃,Java 技術棧路線這種,實際上對於這一類問題我經常不太敢回答,每個人的情況都不太一樣,而小夥伴也很少詳細介紹自己的情況,大都是一兩句話就把問題拋出來了,啥情況都不了解,就要指出一個方向,這實在是太難了。

因此今天我想從我學習 Spring Boot + Vue 這套技術棧的角度,來和大家聊一聊沒有人指導,我是如何一步一步建立起自己的技術體系的。

線上大家看我經常寫文章,線下我其實比較宅,跟人交流比較少,我也很少問別人職業規劃或者技術規劃這些問題,因為這種學什麼的問題,我喜歡自己把握,我不太喜歡被別人牽着走。

Rome was not built in a day,剛開始接觸 Spring Boot + Vue 時,我甚至都沒有一個明確的想法,只是覺得該學點什麼,不能讓時間浪費,沒有告訴我 Spring Boot 要火了,也沒有人告訴我 Vue 要超過 React 了,都是我自己一直在摸索摸索,一步一步,直到構建起這套技術大廈。

Spring Boot

先說說 Spring Boot 吧,三年前差不多也是這個時候,是我第一次接觸 Spring Boot ,那個時候我的正式身份還是一名 Android 工程師,那段時間在研究 Android7 的源碼,還寫了一些博客:

但是那個時候 Android 的行情在慢慢下滑,而我剛畢業 1 年多,未來還有更加豐富的技術人生,我不願意這麼早就把技術棧定死,而且還定在一個行情日漸下滑的技術棧上。所以我打算學一點新的東西。

Python、Go、前端 和 Java 都是備選的方向,但是最終還是選擇繼續做 Java,有三個原因:

  • 做 Java 當時可以在公司內部轉崗,做 Python 或者 Go 的話,可能就得換工作了,技術棧切換,一切從頭開始,當時心裏還是沒底,於是就選擇繼續做 Java
  • 剛好大學的時候也有 JavaEE 的底子,重新撿起來 JavaEE 相關的技術點倒也不是啥難事
  • 第三點也是最重要的一點,我一直希望能夠獨立接點私活,這樣有一天賺錢能夠不受工作地點的限制,基於這樣的初衷,我一直希望走全棧的路線,用 Python 和 Go 雖然也可以做企業級應用,但是在目前的技術環境下,這並不算是主流方案,主流方案依然是 Java ,雖然它被被多人吐槽

基於以上三點,我決定還是走 Java 的方向吧。

2016 年那會,CSDN 幾乎每個月送我一本技術圖書,10 月份的圖書我就和夢鴿美女要了一本 Spring Boot 相關的書,書到了之後,一直在忙各種事情沒時間看,到了當年 12 月份的時候,公司安排我去深圳出差,出差的話,每天下班后時間就比較充裕了,於是我就帶上了書,每天下班回到酒店,就開始搞 Spring Boot。

一開始我就發現這玩意相比我大學時候搞得 XML 配置的 SSM 太好用了,還是 SSM 那套東西,但是有了自動化配置,不用再去寫讓人頭大的 XML 配置了,可以基於 Spring Boot 快速搞一個 SSM 應用出來。不過剛開始學的時候我還不知道 Spring Boot 在 Java 領域如此火爆,當我寫了幾篇博客之後,我發現每篇博客的閱讀量都暴漲,遠遠高於其他博客的閱讀,我隱隱約約感覺到這次稀里糊塗的技術棧切換,算是沒走錯路。

不過老實說,Spring Boot 技術棧其實不算難,都是 SSM 那一套東西,只是多了自動化配置(當然,Spring Boot 也有不少自己的東西,不過整體上基於 SSM 這點應該沒啥爭議),我剛開始搞 Spring Boot 的時候,有時候會有一些東西看的雲里霧裡,後來發現問題出在 Spring + SpringMVC 上,好幾年不寫 JavaEE,這些東西有一點點生疏了,後來又花了一些時間把 SSM 這些東西系統過了一遍,然後再去看 Spring Boot 就順暢多了。

所以有一些小夥伴問松哥能不能跳過 SSM 直接學習 Spring Boot,這個我不建議,大家在 Spring Boot 中見到的很多神奇的自動化配置大部分都是基於 Spring 現有功能實現的,要是不懂實現原理,你會發現 Spring Boot 用得時候雖然好用,但是出了問題,你就束手無策了。

就這樣,跳入了 Spring Boot 的坑裡了。Spring Boot 學完沒多久,工作上,馬上要從 Android 切換到 JavaEE 了,亟需一個項目練練手,當時我的上司給我介紹了一個西藏大學的項目,我使用 Spring Boot+EasyUI 的技術棧花了不到一個禮拜做完了,從此就算是叩開了 JavaEE 的大門,那會是 2017 年。

當時前端選擇 EasyUI 也是沒辦法,甲方催得緊,而我來不及搞其他的前端框架,當時只有 EasyUI 熟悉一些,不用花時間學,直接就能用,於是就選擇了 EasyUI,但是 EasyUI 太丑了,所以在做完西藏大學的項目后,我就一直思量着再整一個專業的前端框架,這樣以後再有私活,我就可以獨立做出來一個好看的後端管理系統了。

就這樣,在綜合對比了 Vue、React 以及 Angular 之後,決定跳入 Vue 的坑。

Vue

前端其實還算接觸的比較早,最早的 jQuery Mobile,PhoneGap 上大學的時候就玩過,我的第一本 NodeJS 的書是在 2013 年買的,那個時候 NodeJS 還算是一個比較新的事物。當我還是一名 Android 工程師的時候,我就玩過 React 和 ReactNative,RN 是當時比較流行的一個跨平台解決方案。但是在我比較這三個技術棧的時候,我發現 Vue 更加好用,生態也更加豐富,而且大有超過 React 的架勢(當時 Vue 在 GitHub 上的 star 數還沒超過 React),於是我就選擇了 Vue。其實當時我心裏想,大不了學完 Vue 再學 React,反正我才剛畢業兩年多,沒必要這麼早就鎖定技術棧停止學習。

Vue 的學習確實不費啥事,花了兩三天時間刷了一遍官網,然後就開始做項目,但是要去深入學習,又是一個漫長的過程了。

Vue 有很多漂亮的 UI 庫,像 ElementUI 等都算是做的比較好的,這些東西只要會用其中一個,其他的就可以手到擒來。

到 2018 年初,Spring Boot+Vue 技術棧基本上已經熟悉了,兩個開源項目 V 部落()和微人事()也受到小夥伴們的歡迎,常規的企業級應用可以一個人獨立完成了,5 月份的時候,經朋友介紹,接了哈爾濱工程大學一位老師的項目,毫無疑問我就使用了最擅長的 Spring Boot+Vue 技術棧來做了,前後端都是自己做,沒人扯皮,美滋滋。

再後來,就是寫書(),業餘繼續搞點項目用 Spring Boot + Vue 來做,這些以前都和大家聊過我就不再多說了,業餘接點項目來做這塊倒是有一些經驗,以後和小夥伴們細聊。

就這樣,沒有任何人的指引,我慢慢構建了 Spring Boot + Vue 這套技術體系,這個過程中,最大的學習經驗就是要寫博客,做筆記,寫博客不僅僅是記錄,也是總結提煉,在寫的過程中,融入自己的思考,加深對技術的理解。 掌握了這套技術棧之後,我覺得我離全棧又更近了一步,離賺錢不受工作地點的限制這個目標也更近一步了。

結語

有前輩大佬的指引,你可能走得快,自己摸索,走的踏實。其實從我第一天自學 Java 開始,基本上都是一直在摸索。大學時候一個 BUG 折騰兩三天才解決,但是一旦自己想明白解決了,以後類似的錯誤不會再犯,這是我的感受。

好了,一點點學習經驗,和小夥伴們分享,要是覺得有啟發,歡迎轉發哦。

掃碼關注松哥,公眾號後台回復 2TB,獲取松哥獨家 超2TB 學習資源

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

【其他文章推薦】

※專營大陸空運台灣貨物推薦

台灣空運大陸一條龍服務

※評比彰化搬家公司費用,南投搬家公司費用收費行情懶人包大公開

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

分類
發燒車訊

020.掌握Pod-Pod基礎使用

一 Pod定義詳解

1.1 完整Pod定義文件

  1 apiVersion: v1			#必選,版本號,例如v1,版本號必須可以用 kubectl api-versions 查詢到
  2 kind: Pod				#必選,Pod
  3 metadata:				#必選,元數據
  4   name: string			#必選,Pod名稱,需符合RFC 1035規範
  5   namespace: string			#必選,Pod所屬的命名空間,默認為"default"
  6   labels:				#自定義標籤
  7     - name: string			#自定義標籤名字
  8   annotations:			#自定義註釋列表
  9     - name: string
 10 spec:				#必選,Pod中容器的詳細定義
 11   containers:			#必選,Pod中容器列表
 12   - name: string			#必選,容器名稱,需符合RFC 1035規範
 13     image: string			#必選,容器的鏡像名稱
 14     imagePullPolicy: [ Always|Never|IfNotPresent ]	#獲取鏡像的策略,Alawys表示每次都嘗試下載鏡像,IfnotPresent表示優先使用本地鏡像,否則下載鏡像,Nerver表示僅使用本地鏡像
 15     command: [string]		#容器的啟動命令列表,如不指定,使用打包時使用的啟動命令
 16     args: [string]			#容器的啟動命令參數列表
 17     workingDir: string		#容器的工作目錄
 18     volumeMounts:			#掛載到容器內部的存儲卷配置
 19     - name: string			#引用pod定義的共享存儲卷的名稱,需用volumes[]部分定義的的卷名
 20       mountPath: string		#存儲卷在容器內mount的絕對路徑,應少於512字符
 21       readOnly: boolean		#是否為只讀模式,默認為讀寫模式
 22     ports:				#需要暴露的端口庫號列表
 23     - name: string			#端口的名稱
 24       containerPort: int		#容器需要監聽的端口號
 25       hostPort: int		        #容器所在主機需要監聽的端口號,默認與Container相同
 26       protocol: string		#端口協議,支持TCP和UDP,默認TCP
 27     env:				#容器運行前需設置的環境變量列表
 28     - name: string			#環境變量名稱
 29       value: string		        #環境變量的值
 30     resources:			#資源限制和請求的設置
 31       limits:			#資源限制的設置
 32         cpu: string		        #CPU的限制,單位為core數,將用於docker run --cpu-shares參數
 33         memory: string		#內存限制,單位可以為Mib/Gib,將用於docker run --memory參數
 34       requests:			#資源請求的設置
 35         cpu: string		        #CPU請求,容器啟動的初始可用數量
 36         memory: string		#內存請求,容器啟動的初始可用數量
 37     livenessProbe:			#對Pod內各容器健康檢查的設置,當探測無響應幾次后將自動重啟該容器,檢查方法有exec、httpGet和tcpSocket,對一個容器只需設置其中一種方法即可
 38       exec:			        #對Pod容器內檢查方式設置為exec方式
 39         command: [string]		#exec方式需要制定的命令或腳本
 40       httpGet:			#對Pod內個容器健康檢查方法設置為HttpGet,需要制定Path、port
 41         path: string
 42         port: number
 43         host: string
 44         scheme: string
 45         HttpHeaders:
 46         - name: string
 47           value: string
 48       tcpSocket:			#對Pod內個容器健康檢查方式設置為tcpSocket方式
 49          port: number
 50        initialDelaySeconds: 0	#容器啟動完成后首次探測的時間,單位為秒
 51        timeoutSeconds: 0		#對容器健康檢查探測等待響應的超時時間,單位秒,默認1秒
 52        periodSeconds: 0		#對容器監控檢查的定期探測時間設置,單位秒,默認10秒一次
 53        successThreshold: 0
 54        failureThreshold: 0
 55        securityContext:
 56          privileged: false
 57     restartPolicy: [Always | Never | OnFailure]	#Pod的重啟策略,Always表示一旦不管以何種方式終止運行,kubelet都將重啟,OnFailure表示只有Pod以非0退出碼退出才重啟,Nerver表示不再重啟該Pod
 58     nodeSelector: obeject		#設置NodeSelector表示將該Pod調度到包含這個label的node上,以key:value的格式指定
 59     imagePullSecrets:		#Pull鏡像時使用的secret名稱,以key:secretkey格式指定
 60     - name: string
 61     hostNetwork: false		#是否使用主機網絡模式,默認為false,如果設置為true,表示使用宿主機網絡
 62     volumes:			#在該pod上定義共享存儲卷列表
 63     - name: string			#共享存儲卷名稱 (volumes類型有很多種)
 64       emptyDir: {}			#類型為emtyDir的存儲卷,與Pod同生命周期的一個臨時目錄。為空值
 65       hostPath: string		#類型為hostPath的存儲卷,表示掛載Pod所在宿主機的目錄
 66         path: string		#Pod所在宿主機的目錄,將被用於同期中mount的目錄
 67       secret:			#類型為secret的存儲卷,掛載集群與定義的secre對象到容器內部
 68         scretname: string
 69         items:
 70         - key: string
 71           path: string
 72       configMap:			#類型為configMap的存儲卷,掛載預定義的configMap對象到容器內部
 73         name: string
 74         items:
 75         - key: string
 76           path: string

二 Pod的基本用法

2.1 創建Pod


Pod可以由1個或多個容器組合而成,通常對於緊耦合的兩個應用,應該組合成一個整體對外提供服務,則應該將這兩個打包為一個pod。

屬於一個Pod的多個容器應用之間相互訪問只需要通過localhost即可通信,這一組容器被綁定在一個環境中。

  1 [root@k8smaster01 study]# vi frontend-localredis-pod.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: redis-php
  6   label:
  7     name: redis-php
  8 spec:
  9   containers:
 10   - name: frontend
 11     image: kubeguide/guestbook-php-frontend:localredis
 12     ports:
 13     - containersPort: 80
 14   - name: redis-php
 15     image: kubeguide/redis-master
 16     ports:
 17     - containersPort: 6379
 18 
 19 [root@k8smaster01 study]# kubectl create -f frontend-localredis-pod.yaml
 20 


2.2 查看Pod

  1 [root@k8smaster01 study]# kubectl get pods	                #READY為2/2,表示此Pod中運行了yaml定義的兩個容器
  2 NAME        READY   STATUS    RESTARTS   AGE
  3 redis-php   2/2     Running   0          7m45s
  4 [root@k8smaster01 study]# kubectl describe pod redis-php	#查看詳細信息
  5 


三 靜態Pod

3.1 靜態Pod概述


靜態pod是由kubelet進行管理的僅存在於特定Node的Pod上,他們不能通過API Server進行管理,無法與ReplicationController、Deployment或者DaemonSet進行關聯,並且kubelet無法對他們進行健康檢查。靜態Pod總是由kubelet進行創建,並且總是在kubelet所在的Node上運行。

創建靜態Pod有兩種方式:配置文件或者HTTP方式。

3.2 配置文件方式創建

  1 [root@k8snode01 ~]# mkdir -p /etc/kubelet.d
  2 [root@k8snode01 ~]# vi /etc/kubelet.d/static-web.yaml
  3 apiVersion: v1
  4 kind: Pod
  5 metadata:
  6   name: static-web
  7   label:
  8     name: static-web
  9 spec:
 10   containers:
 11   - name: static-web
 12     image: nginx
 13     ports:
 14     - name: web
 15       containersPort: 80
 16 
 17 [root@k8snode01 ~]# vi /etc/systemd/system/kubelet.service
 18 ……
 19   --config=/etc/kubelet.d/ \·				#加入此參數
 20 ……
 21 [root@k8snode01 ~]# systemctl daemon-reload
 22 [root@k8snode01 ~]# systemctl restart kubelet.service	#重啟kubelet
 23 [root@k8snode01 ~]# docker ps				#查看創建的pod



提示:由於靜態pod不能通過API Server進行管理,因此在Master節點執行刪除操作後會變為Pending狀態,且無法刪除。刪除該pod只能在其運行的node上,將定義POD的yaml刪除。

3.3 HTTP方式


通過設置kubelet的啟動參數–mainfest-url,會定期從該URL下載Pod的定義文件,並以.yaml或.json文件的格式進行解析,從而創建Pod。

四 Pod容器共享Volume

4.1 共享Volume


在同一個Pod中的多個容器能夠共享Pod級別的存儲就Volume。Volume可以被定義為各種類型,多個容器各自進行掛載操作,將一個Volume掛載為容器內部需要的目錄。


示例1:

Pod級別設置Volume “app-logs”,同時Pod包含兩個容器,Tomcat向該Volume寫日誌,busybox讀取日誌文件。

  1 [root@k8smaster01 study]# vi pod-volume-applogs.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: volume-pod
  6 spec:
  7   containers:
  8   - name: tomcat
  9     image: tomcat
 10     ports:
 11     - containerPort: 8080
 12     volumeMounts:
 13     - name: app-logs
 14       mountPath: /usr/local/tomcat/logs
 15   - name: logreader
 16     image: busybox
 17     command: ["sh","-c","tail -f /logs/catalina*.log"]
 18     volumeMounts:
 19     - name: app-logs
 20       mountPath: /logs
 21   volumes:
 22   - name: app-logs
 23     emptyDir: {}

解釋:

Volume名:app-logs;

emptyDir:為Pod分配到Node的時候創建。無需指定宿主機的目錄文件,為Kubernetes自動分配的目錄。

  1 [root@k8smaster01 study]# kubectl create -f pod-volume-applogs.yaml	#創建
  2 [root@k8smaster01 study]# kubectl get pods				#查看
  3 [root@k8smaster01 study]# kubectl logs volume-pod -c busybox	#讀取log




  1 [root@k8smaster01 study]# kubectl exec -it volume-pod -c tomcat -- ls /usr/local/tomcat/logs
  2 catalina.2019-06-29.log      localhost_access_log.2019-06-29.txt
  3 host-manager.2019-06-29.log  manager.2019-06-29.log
  4 localhost.2019-06-29.log
  5 [root@k8smaster01 study]# kubectl exec -it volume-pod -c tomcat -- tail /usr/local/tomcat/logs/catalina.2019-06-29.log



提示:通過tomcat容器可查看日誌,對比busybox通過共享Volume查看的日誌是否一致。

五 Pod配置管理

5.1 Pod配置概述


應用部署的一個最佳實踐是將應用所需的配置信息與程序進行分離,使程序更加靈活。將相應的應用打包為鏡像,可以通過環境變量或者外掛volume的方式在創建容器的時候進行配置注入,從而實現更好的復用。

Kubernetes提供一種統一的應用配置管理方案:ConfigMap。

5.2 ConfigMap概述


ConfigMap供容器使用的主要場景:

  • 生成容器內部的環境變量;
  • 設置容器的啟動命令的參數(需設置為環境變量);
  • 以volume的形式掛載為容器內部的文件或者目錄。


ConfigMap以一個或多個key:value的形式定義。value可以是string也可以是一個文件內容,可以通過yaml配置文件或者通過kubectl create configmap 的方式創建configMap。

5.3 創建ConfigMap資源對象——yaml方式

  1 [root@k8smaster01 study]# vi cm-appvars.yaml
  2 apiVersion: v1
  3 kind: ConfigMap
  4 metadata:
  5   name: cm-appvars
  6 data:
  7   apploglevel: info
  8   appdatadir: /var/data
  9 
 10 [root@k8smaster01 study]# kubectl create -f cm-appvars.yaml
 11 configmap/cm-appvars created
 12 [root@k8smaster01 study]# kubectl get configmaps
 13 NAME         DATA   AGE
 14 cm-appvars   2      8s
 15 [root@k8smaster01 study]# kubectl describe configmaps cm-appvars



  1 [root@k8smaster01 study]# kubectl get configmaps cm-appvars -o yaml


5.4 創建ConfigMap資源對象——命令行方式


語法1

  1 # kubectl create configmap NAME --from-file=[key=]source --from-file=[key=]source



解釋:通過–from-file參數從文件中創建,可以指定key名稱,也可以制定多個key。

語法2

  1 # kubectl create configmap NAME --from-file=config-files-dir



解釋:通過–from-file參數從目錄中創建,該目錄下的每個配置文件名都被設置為key,文件的內容被設置為value。

語法3

  1 # kubectl create configmap NAME --from-literal=key1=value1 --from-literal=key2=value2



解釋:通過–from-literal參數從文本中創建,直接將指定的key#=value#創建為ConfigMap的內容。

5.5 Pod使用ConfigMap


容器應用使用ConfigMap有兩種方式:

  • 通過環境變量獲取ConfigMap中的內容;
  • 通過Volume掛載的方式將ConfigMap中的內容掛載為容器內容的文件或目錄。

  1 [root@k8smaster01 study]# vi cm-test-pod.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: cm-test-pod
  6 spec:
  7   containers:
  8   - name: cm-test
  9     image: busybox
 10     command: ["/bin/sh","-c","env|grep APP"]	#容器里執行查看環境變量的命令
 11     env:
 12     - name: APPLOGLEVEL				#定義容器環境變量名稱
 13       valueFrom:
 14         configMapKeyRef:			#環境變量的值來自ConfigMap
 15           name: cm-appvars			#指定來自cm-appvars的ConfigMap
 16           key: apploglevel			#key為apploglevel
 17     - name: APPDATADIR
 18       valueFrom:
 19         configMapKeyRef:
 20           name: cm-appvars
 21           key: appdatadir
 22 
 23 [root@k8smaster01 study]# kubectl create -f cm-test-pod.yaml
 24 [root@k8smaster01 study]# kubectl get pods
 25 NAME          READY   STATUS      RESTARTS   AGE
 26 cm-test-pod   0/1     Completed   2          24s



【掛載形式-待補充】

5.6 ConfigMap限制


  • Configmap必須在pod創建之間創建;
  • ConfigMap受到namespace的限制,只有同一個命名空間下才能引用;
  • ConfigMap暫時無法配置配額;
  • 靜態的pod無法使用ConfigMap;
  • 在使用volumeMount掛載的時候,configMap基於items創建的文件會整體的將掛載數據卷的容器的目錄下的文件全部覆蓋。

六 Pod獲取自身信息

6.1 Downward API


pod擁有唯一的名字、IP地址,並且處於某個Namespace中。pod的容器內獲取pod的信息科通過Downward API實現。具體有以下兩種方式:

  • 環境變量:用於單個變量,可以將pod信息和container信息注入容器內部;
  • volume掛載:將數組類信息生成為文件,掛載至容器內部。


舉例1:通過Downward API將Pod的IP、名稱和所在的Namespace注入容器的環境變量。

  1 [root@k8smaster01 study]# vi dapi-test-pod.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: dapi-test-pod
  6 spec:
  7   containers:
  8     - name: test-container
  9       image: busybox
 10       command: [ "/bin/sh", "-c", "env" ]
 11       env:
 12         - name: MY_POD_NAME
 13           valueFrom:
 14             fieldRef:
 15               fieldPath: metadata.name
 16         - name: MY_POD_NAMESPACE
 17           valueFrom:
 18             fieldRef:
 19               fieldPath: metadata.namespace
 20         - name: MY_POD_IP
 21           valueFrom:
 22             fieldRef:
 23               fieldPath: status.podIP
 24   restartPolicy: Never



提示:Downward API提供如下變量:

metadata.name:Pod的名稱,當Pod通過RC生成時,其名稱是RC隨機產生的唯一名稱;

status.podIP:Pod的IP地址,POd的IP屬於狀態數據,而非元數據;

metadata.namespace:Pod所在的namespace。

  1 [root@k8smaster01 study]# kubectl create -f dapi-test-pod.yaml
  2 [root@k8smaster01 study]# kubectl logs dapi-test-pod | grep MY_POD
  3 MY_POD_NAMESPACE=default
  4 MY_POD_IP=172.30.240.4
  5 MY_POD_NAME=dapi-test-pod
  6 



舉例2:通過Downward API將Container的自願請求和限制信息注入容器的環境變量。

  1 [root@k8smaster01 study]# vi dapi-test-pod-container-vars.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: dapi-test-pod-container-vars
  6 spec:
  7   containers:
  8     - name: test-container
  9       image: busybox
 10       imagePullPolicy: Never
 11       command: [ "/bin/sh", "-c" ]
 12       args:
 13       - while true; do
 14           echo -en '\n';
 15           printenv MY_CPU_REQUEST MY_CPU_LIMIT;
 16           printenv MY_MEM_REQUEST MY_MEM_LIMIT;
 17           sleep 3600;
 18         done;
 19       resources:
 20         requests:
 21           memory: "32Mi"
 22           cpu: "125m"
 23         limits:
 24           memory: "64Mi"
 25           cpu: "250m"
 26       env:
 27         - name: MY_CPU_REQUEST
 28           valueFrom:
 29             resourceFieldRef:
 30               containerName: test-container
 31               resource: requests.cpu
 32         - name: MY_CPU_LIMIT
 33           valueFrom:
 34             resourceFieldRef:
 35               containerName: test-container
 36               resource: limits.cpu
 37         - name: MY_MEM_REQUEST
 38           valueFrom:
 39             resourceFieldRef:
 40               containerName: test-container
 41               resource: requests.memory
 42         - name: MY_MEM_LIMIT
 43           valueFrom:
 44             resourceFieldRef:
 45               containerName: test-container
 46               resource: limits.memory
 47   restartPolicy: Never



提示:Downward API提供如下變量:

requests.cpu:容器的CPU請求值;

limits.cpu:容器的CPU限制值;

requests.memory:容器的內存請求值;

limits.memory:容器的內存限制值。

  1 [root@k8smaster01 study]# kubectl create -f dapi-test-pod-container-vars.yaml
  2 [root@k8smaster01 study]# kubectl logs dapi-test-pod-container-vars
  3 1
  4 1
  5 33554432
  6 67108864



舉例3:通過Downward API將Pod的Label、Annotation列表通過Volume掛載為容器內的一個文件。

  1 [root@k8smaster01 study]# vi dapi-test-pod-volume.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: dapi-test-pod-volume
  6   labels:
  7     zone: us-est-coast
  8     cluster: test-cluster1
  9     rack: rack-22
 10   annotations:
 11     build: two
 12     builder: john-doe
 13 spec:
 14   containers:
 15     - name: test-container
 16       image: busybox
 17       imagePullPolicy: Never
 18       command: [ "/bin/sh", "-c" ]
 19       args:
 20       - while true; do
 21           if [[ -e /etc/labels ]]; then
 22             echo -en '\n\n'; cat /etc/labels; fi;
 23           if [[ -e /etc/annotations ]]; then
 24             echo -en '\n\n'; cat /etc/annotations; fi;
 25           sleep 3600;
 26         done;
 27       volumeMounts:
 28         - name: podinfo
 29           mountPath: /etc
 30           readOnly: false
 31   volumes:
 32     - name: podinfo
 33       downwardAPI:
 34         items:
 35           - path: "labels"
 36             fieldRef:
 37               fieldPath: metadata.labels
 38           - path: "annotations"
 39             fieldRef:
 40               fieldPath: metadata.annotations



注意:Volume中的ddownwardAPI的items語法,將會以path的名稱生成文件。如上所示,會在容器內生產/etc/labels和/etc/annotations兩個文件,分別包含metadata.labels和metadata.annotations的全部Label。

  1 [root@k8smaster01 study]# kubectl create -f dapi-test-pod-volume.yaml
  2 [root@k8smaster01 study]# kubectl logs dapi-test-pod-volume
  3 



提示:DownwardAPI意義:

在某些集群中,集群中的每個節點需要將自身的標識(ID)及進程綁定的IP地址等信息事先寫入配置文件中,進程啟動時讀取此類信息,然後發布到某個類似註冊服務中心。此時可通過DowanwardAPI,將一個預啟動腳本或Init Container,通過環境變量或文件方式獲取Pod自身的信息,然後寫入主程序配置文件中,最後啟動主程序。本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

分類
發燒車訊

SpringBoot基本配置詳解

SpringBoot項目有一些基本的配置,比如啟動圖案(banner),比如默認配置文件application.properties,以及相關的默認配置項。

示例項目代碼在:

一、啟動圖案banner

編寫banner.txt放入resources文件夾下,然後啟動項目即可修改默認圖案。

關於banner的生成,可以去一些專門的網站。

比如:https://www.bootschool.net/ascii

二、配置文件application

2.1 application.properties/yml

resources下通常會默認生成一個application.properties文件,這個文件包含了SpringBoot項目的全局配置文件。裏面的配置項通常是這樣的:

server.port=8080

在這個文件里我們可以添加框架支持的配置項,比如項目端口號、JDBC連接的數據源、日誌級別等等。

現在比較流行的是將properties文件改為yml文件。yml文件的格式yaml是這樣的:

server:
    port: 8080

yml和properties的作用是一樣的。而yml的好處是顯而易見的——更易寫易讀。

屬性之間互相調用使用${name}:

eknown:
    email: eknown@163.com
    uri: http://www.eknown.cn
    title: 'hello, link to ${eknown.uri} or email to ${eknown.email}'

鏈接:

2.2 多環境配置文件

通常開發一個應用會有多個環境,常見如dev/prod,也會有test,甚至其他一些自定義的環境,SpringBoot支持配置文件的靈活切換。

定義新配置文件需要遵循以下格式:application-{profile}.properties 或者application-{profile}.yml

比如現在有dev和prod兩個環境,我需要在application.yml文件之外新建兩個文件:

  1. application-dev.yml

    server:
       port: 8080
  2. application-prod.yml

    server:
      port: 8081

然後在application.yml中通過application.profiles.active={profile}指明啟用那個配置:

application:
    profiles:
      active: dev

除了在application.yml中指定配置文件外,還可以通過啟動命令指定:java -jar xxx.jar --spring.profiles.active=dev

2.2 自定義配置項並獲取它

主要介紹兩種方式,獲取單個配置項和獲取多個配置項。

舉例:

eknown:
    email: eknown@163.com
    uri: http://www.eknown.cn

2.2.1 使用@Value註解獲取單個配置項

@Value("${eknown.email}")
private String email;

@Value("${eknown.uri}")
private String url;

注意:使用@Value註解的時候,所在類必須被Spring容器管理,也就是被@Component、@Controller、@Service等註解定義的類。

2.2.2 獲取多個配置項

第一種,定義一個bean類,通過@Value獲取多個配置項:

@Component
public class MyConfigBean {
  
}

然後我們通過get方法來獲取這些值:

@RestController
public class BasicAction {
  
  @Autowired
  private MyConfigBean myConfigBean;

}

第二種,使用註解@ConfigurationProperties:

@Component
@ConfigurationProperties(perfix="eknown")
public class MyConfigBean {

  private String email;
  private String uri;
}

這裏只需要通過prefix指定前綴即可,後面的值自動匹配。

這裏我們還使用了@Component註解來讓spring容器管理這個MyConfigBean。

此外,我們可以不需要引入@Component,轉而在Application啟動類上加上@EnableConfigurationProperties({MyConfigBean.class})來啟動這個配置。

注意:我們這裡是從主配置文件,也就是SpringBoot默認的application-profile文件中獲取配置數據的。

而從自定義的配置文件,比如test.yml這種形式中獲取配置項時,情況是有點不大一樣的。

三、自定義配置文件

上面介紹的配置文件都是springboot默認的application開頭的文件。如果要自定義一個配置文件呢,比如test.yml或test.properties,怎麼獲取其中的配置項呢?

使用@PageResource註解即可。

首先我們來看一下讀取自定義的properties文件里的內容:

test.properties

hello.time=2019.11.19
hello.name=eknown

定義Configuration類:

@Configuration
@PropertySource("classpath:test.properties")
//@PropertySource("classpath:test.yml") // 注意,yml文件不能直接這樣寫,會讀不出數據
@ConfigurationProperties(prefix = "hello")
public class TestConfiguration {
    private String name;
    private String time;

    // hide get and set methods
}

測試一下:

@RestController
@RequestMapping(value = "test")
public class TestAction {

    @Autowired
    private TestConfiguration testConfiguration;

    @GetMapping(value = "config")
    public String test() {
        return testConfiguration.getName() + "<br/>" + testConfiguration.getTime();
    }
}

如果將properties文件換成yml文件呢?

我們嘗試一下,發現:

讀不出數據?

分析一下@PropertySource註解,發現其使用的PropertySourceFactory是DefaultPropertySourceFactory.

這個類的源碼如下:

public class DefaultPropertySourceFactory implements PropertySourceFactory {
    public DefaultPropertySourceFactory() {
    }

    public PropertySource<?> createPropertySource(@Nullable String name, EncodedResource resource) throws IOException {
        return name != null ? new ResourcePropertySource(name, resource) : new ResourcePropertySource(resource);
    }
}

這個類只能處理properties文件,無法處理yml文件。所以我們需要自定義一個YmlSourceFactory。

public class YamlSourceFactory extends DefaultPropertySourceFactory {

    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
        return new YamlPropertySourceLoader().load(resource.getResource().getFilename()
                , resource.getResource()).get(0);
    }
}

然後定義test.yml文件的config類:

@Configuration
@PropertySource(value = "classpath:test.yml", encoding = "utf-8", factory = YamlSourceFactory.class)
@ConfigurationProperties(prefix = "yml.hello")
public class TestYamlConfiguration {
    private String name;
    private String time;

    // hide get and set methods
}

注:為了區分test.properties和test.yml,這裏的test.yml中的屬性以yml.hello開頭。

編寫一下測試:

    @Autowired
    private TestYamlConfiguration ymlConfiguration;

    @GetMapping(value = "yml")
    public String testYml() {
        return "yml config: <br/>" + ymlConfiguration.getName() + "<br/>" + ymlConfiguration.getTime();
    }

訪問:

四、補充@ConfigurationProperties

網上一些資料中,為配合使用@ConfigurationProperties,還使用了@EnableConfigurationProperties註解。

經過測試發現:

  1. 從SpringBoot默認配置文件讀取配置信息,使用@ConfigurationProperties + @Component/@Configuration,或者@ConfigurationProperties + 在啟動類添加@EnableConfigurationProperties({class})。這兩種方式都能解決問題

  2. 從非默認配置文件讀取配置信息,需要利用@PropertySource註解。同樣兩種方式:

    2.1 @PropertySource + @ConfigurationProperties + @Component/@Configuration

    2.2 @PropertySource + @ConfigurationProperties + @Component/@Configuration + @EnableConfigurationProperties,第二種方式存在一個問題,即還是必須要使用@Component註解,如果不使用,則會導致讀取配置信息為null,但程序不會報錯;而如果採用了,則會導致bean類的set方法被執行兩次(也就是生成了兩個同樣類型的bean類)。這種方式不建議!

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

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

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

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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

※專營大陸快遞台灣服務

台灣快遞大陸的貨運公司有哪些呢?