0%

利用APCu作为本地缓存,同时使用Memcahed将缓存分布到多台服务器上,提高性能。

安装APCu

Hat/CentOS/Fedora:

yum install -y php-pecl-apcu

systemctl restart httpd

Debian/Ubuntu/Mint:

apt-get install php5-apcu/trusty-backports

systemctl restart httpd

安装Memcached和php-pecl-memcached

方法一:编译安装

安装编译器

yum -y install gcc gcc-c++

从官网中下载最新的 memcached http://www.memcached.org/

tar -xvf memcached-1.4.15.tar.gz

cd memcached-1.4.15

./configure –prefix=/usr/local/memcache

出现了 configure: error: libevent is required. You can get it from http://www.monkey.org/~provos/libevent/

就直接去,那个网站下载

tar zxvf libevent-1.2.tar.gz

cd libevent-1.2

./configure -prefix=/usr

make

make install

编译安装php模块的memcache模块 下载地址 http://pecl.php.net/package/memcache

tar -xvf memcache-2.2.7.tar.gz

cd memcache-2.2.7

#/usr/local/php/bin/phpize

#./configure –with-php-config=/usr/local/php/bin/php-config –enable-memcache

make & make install

#/usr/local/bin/memcached -d -m 10 -u root -l 192.168.12.201 -p 13001 -c 256 -P /tmp/memcached.pid #启动memcache

方法二:yum/apt-get安装

Hat/CentOS/Fedora:

yum install memcached php-pecl-memcached

systemctl start memcached

Debian/Ubuntu/Mint:

apt-get install memcached php5-memcached

systemctl start memcached

重启apache:

systemctl restart httpd #centos/hat/fedora

systemctl restart apache2 #ubuntu/debain/mint

您可以验证Memcached守护程序是否正在使用ps ax运行:

ps ax | grep memcached
19563 ? Sl 0:02 /usr/bin/memcached -m 64 -p 11211 -u memcache -l
127.0.0.1

配置config.php文件

编辑config.php文件

vim /var/www/html/nextcloud/config/config.php

);前添加下面的的代码:

‘memcache.local’ => ‘\OC\Memcache\APCu’,
‘memcache.distributed’ => ‘\OC\Memcache\Memcached’,
‘memcached_servers’ => array(
array(‘localhost’, 11211),
array(‘server1.example.com’, 11211),
array(‘server2.example.com’, 11211),
),

其中

array(‘server1.example.com’, 11211),
array(‘server2.example.com’, 11211),

指向第二、第三台memcached服务器(如果没有,这两行必须删去。同理,按以上格式可以增加更多memcached服务器),实现将数据缓存分布到多个服务器上。

如图:

检查

登录nextcloud,进入http://ip/nextcloud/index.php/settings/admin

APCu+Memcahed已开启

文章目录

  1. 安装APCu

  2. 安装Memcached和php-pecl-memcached

  3. 方法一:编译安装

  4. 方法二:yum/apt-get安装

  5. 配置config.php文件

  6. 检查

本文由 橙叶博客 作者:FrankGreg 发表,转载请注明来源!

FrankGreg

[

FrankGreg

](https://www.orgleaf.com/author/1 “FrankGreg”)文章:260 画廊:7 视频:11

……

热评文章

最赞的文章

我最近打算学习WPF ,在寻找MVVM框架的时候发现了PRISM,在此之前还从一些博客上了解了其他的MVVM框架,比如浅谈WPF中的MVVM框架–MVVMFoundation 中提到的MVVMFoundation,再比如 ViewModel从未如此清爽 - 轻量级WPF MVVM框架Stylet 中的Stylet。在知道PRISM是微软自家的框架的时候,就毫不犹豫的选了他,即便个人很倾向于可爱的Stylet。作为初学者,对WPF都没有很好的了解的情况下,去学习使用一个_WPF_的框架是否真的大丈夫?好在我发现了这个Prism-Samples-Wpf,既然是WPF的应用,是否也可以作为学习WPF的示例呢?而且Prism的中文资料很少,园子里的也比较老(这货貌似很少有人用)对新手不是太友好,所以我就将我学习Prism及WPF的过程记录下来。如有错误或者理解偏颇的地方,希望大家指正。

0x0 WPF?#

维基百科中的定义:

Windows Presentation Foundation(WPF)是美国微软公司推出.NET Framework 3.0及以后版本的组成部分之一,它是一套基于XML.NET Framework向量绘图技术的展示层开发框架,微软视其为下一代用户界面技术,广泛被用于Windows Vista的界面开发。

WPF使用一种新的XAML(eXtensible Application Markup Language)语言来开发界面,这将把界面开发以及后台逻辑很好的分开,降低了耦合度,使用户界面设计师与程序开发者能更好的合作,降低维护和更新的成本。

如果你有C# WinForms基础,WPF貌似就很好理解,他提供了一种界面开发方案,将界面与后台代码分开,而且WPF更加美观相比丑陋的WinForms,而且,他能更好的支持缩放(高DPI下的WinForms应用简直惨不忍睹)。

WPF像WinForms一样可以自由拖放控件,在Toolbox中可以看到他支持大部分常用控件,跟WinForms的区别就是中间设计器部分,他分Design和XAML。

每一个xaml都配有一个xaml.cs的后台文件,这个被称为 code-behind(新建的第一个WPF项目中的App.xaml.cs还有MainWindow.xaml.cs 这类都是),这里面就是我们熟悉的C#代码。

我们先看看这两个code-behind里的内容:

为了篇幅紧凑,省去了using部分

App.xaml.cs:

1
2
3
4
5
6
7
8
9
namespace WpfApp1
{



public partial class App : Application
{
}
}

MainWindow.xaml.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace WpfApp1
{



public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}

好像少了点什么?

Main方法呢?开始我也这么觉得,直到我尝试着自己在App.xaml.cs中写了一个Main方法,然后编译的时候报了个错:

1
2
CS0111Type 'App' already defines a member called 'Main' with the same parameter typesWpfApp1C:\Users\Yq\source\repos\DotnetTest\WpfApp1\obj\Debug\App.g.cs

原来在这里!接下来我们看一下这个App.g.cs

为了篇幅紧凑,依旧省去了部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using WpfApp1;


namespace WpfApp1 {





public partial class App : System.Windows.Application {




[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "4.0.0.0")]
public void InitializeComponent() {

#line 5 "..\..\App.xaml"
this.StartupUri = new System.Uri("MainWindow.xaml", System.UriKind.Relative);

#line default
#line hidden
}




[System.STAThreadAttribute()]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "4.0.0.0")]
public static void Main() {
WpfApp1.App app = new WpfApp1.App();
app.InitializeComponent();
app.Run();
}
}
}

虽然看不太懂,但是,不管怎样,我们看到了我们熟悉的Main方法,他的作用也很明确 ,Application Entry Point. InitializeComponent方法初始化了StartupUri,他是在 App.xaml中定义的:

1
StartupUri="MainWindow.xaml"

0x07交互#

[7.1updated]无变化
这是这个系列的最后一篇了,主要介绍了Prism中为我们提供几种弹窗交互的方式。

Notification通知式#

Prism通过InteractionRequest 来实现弹窗交互,它是一个泛型接口,不同的类型对应不同类型的弹窗方式。
在使用InteractionRequest的时候需要在,xaml中需要注册一个Trigger:

1
2
3
4
5
<i:Interaction.Triggers>
<prism:InteractionRequestTrigger SourceObject="{Binding NotificationRequest}">
<prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True" />
</prism:InteractionRequestTrigger>
</i:Interaction.Triggers>
Interaction

这里用到了Interaction,他是i命名空间里的东西,那么i是什么呢?
interactivity这个是微软内置的类库,他提供了一组用户交互的类,比如我们这里用到的EventTrigger可以用来执行事件触发的操作。
在使用的时候,先引入xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
或者xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity",然后在xaml中使用他:

1
2
3
4
5
<i:Interaction.Triggers>
<i:EventTrigger>

</i:EventTrigger>
</i:Interaction.Triggers>

而 prism:PopupWindowAction 的 IsModal=True意味着弹框不被关闭的时候,父窗体无法使用。我刚搜索了一下,这个词的翻译竟然“模态”。

模态对话框(Modal Dialogue Box,又叫做模式对话框),是指在用户想要对对话框以外的应用程序进行操作时,必须首先对该对话框进行响应。 如单击【确定】或【取消】按钮等将该对话框关闭。

好,接着,我们在code-behind中声明,使用INotification类型:

1
public InteractionRequest<INotification> NotificationRequest { get; set; }

在command的回调函数中就可以使用NotificationRequest:

1
NotificationRequest.Raise(new Notification { Content = "Notification Message", Title = "Notification" }, r => Title = "Notified");

最后通过ConfirmationRequest.Raise()方法来实现调用弹窗,这里将Title修改为“Notified”。

Confirmation 确认式#

跟Notification的使用方法一样,先注册Trigger:

1
2
3
<prism:InteractionRequestTrigger SourceObject="{Binding ConfirmationRequest}">
<prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True" />
</prism:InteractionRequestTrigger>

然后在使用InteractionRequest的时候使用IConfirmation类型:

1
public InteractionRequest<IConfirmation> ConfirmationRequest { get; set; }

callback:

1
2
3
4
ConfirmationRequest.Raise(new Confirmation {
Title = "Confirmation",
Content = "Confirmation Message" },
r => Title = r.Confirmed ? "Confirmed" : "Not Confirmed");

原本一直好奇为什么r能获取confirmationconfirmed属性,后来才发现,自学这个东西,急于求成是不行的。
看下prism的 ConfirmationRequest.Raise()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14





[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate")]
public void Raise(T context, Action<T> callback)
{
var handler = this.Raised;
if (handler != null)
{
handler(this, new InteractionRequestedEventArgs(context, () => { if(callback != null) callback(context); } ));
}
}

上面的通知和提示窗体,都是内置的,很多时候,我们需要自制一些弹窗来满足更复杂的使用场景,比如我们通过弹窗来传递一些信息,贴心的Prism同样为我们准备了一个接口IInteractionRequestAware:

1
2
3
4
5
6
7
8
9




INotification Notification { get; set; }



Action FinishInteraction { get; set; }

蛤蛤,Notification正是我们需要的东西,再看看他是什么鬼

1
2
3
4
5
6
7
8



string Title { get; set; }



object Content { get; set; }

原来这个被用来传递的东西,也有标准,需要一个名字和一个内容,内容是object,也就是,我们可以用他来装下任何东西。

FinishInteractioninvoke来关闭交互界面,这个很简单,具体他怎么关闭的暂时不看了。接下来,我们大概有一个思路了:
首先,先定义好我们需要数据载体类实现INotification,再设计一个usercontrole,他的vm实现IInteractionRequestAware接口,然后在NotificationRequest.Raise的时候使用usercontrole,美滋滋!!!😆
我们来通过客制化弹窗,实现从一个字符串列表中选择一个字符串,返回给父窗体:

先设计Notification,他并没有直接实现INotification,而是实现了IConfirmation,IConfirmationINotification的基础上,添加了一个Confirmed属性,来获取弹窗返回状态,是布尔型的(布尔型只有两种状态,很多时候,我需要有战斗机一般多的按钮的时候,他就不够用了,到时候得重新设计一个枚举类型的),这里,我们就直接实现IConfirmation(_为什么先是搞了一个接口呢?当然是为了依赖注入啊!依赖注入很难讲,以前我也看了很多大佬的资料,但是没有懂,后来去问大佬,大佬说,你看懂了吗?我说似懂非懂,他说,那就去看代码吧,慢慢的就懂了。😂_):

1
2
3
4
5
6
7
8
9
using Prism.Interactivity.InteractionRequest;

namespace UsingPopupWindowAction.Notifications
{
public interface ICustomNotification : IConfirmation
{
string SelectedItem { get; set; }
}
}

接着是我们的实现类(一个list(源),一个string(目标))继承 Confirmation实现我们的接口ICustomNotification,继承 Confirmation是因为他继承自Notification,而Notification是实现了INotification的,这样,我们就在我们的类里不用去实现INotification了,其实也可以不用继承·Confirmation·,完全可以自己实现ICustomNotification他所有的接口(话说若干年前我怎么记得接口不可以被继承只能被实现呢?记错了?):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using Prism.Interactivity.InteractionRequest;
using System.Collections.Generic;

namespace UsingPopupWindowAction.Notifications
{
public class CustomNotification : Confirmation, ICustomNotification
{
public IList<string> Items { get; private set; }

public string SelectedItem { get; set; }

public CustomNotification()
{
this.Items = new List<string>();
this.SelectedItem = null;

CreateItems();
}

private void CreateItems()
{

}
}
}

如果不继承Confirmation,则需要添加部分实现:

1
2
3
4

public bool Confirmed { get ; set ; }
public string Title { get ; set ; }
public object Content { get ; set ; }

接下来设计我们的弹窗(一个列表(显示源),两个按钮,一个取消一个提交(获取目标)):

1
2
3
4
5
6
7
8
9
10
11
12
<TextBlock Margin="10" TextWrapping="Wrap" FontWeight="Bold">Please select an item:</TextBlock>
<ListBox SelectionMode="Single" Margin="10,0" Height="100" ItemsSource="{Binding Notification.Items}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}"></ListBox>

<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>

<Button AutomationProperties.AutomationId="ItemsSelectButton" Grid.Column="0" Margin="10" Command="{Binding SelectItemCommand}">Select Item</Button>
<Button AutomationProperties.AutomationId="ItemsCancelButton" Grid.Column="1" Margin="10" Command="{Binding CancelCommand}">Cancel</Button>
</Grid>

弹窗的ViewModel,实现IInteractionRequestAware接口,这里设置了一个_notification来接收我们用来传递的那个类,这很像MVC里的Model,他只是个数据的载体,在每一个命令最后都需要关闭窗体,并且之前对confirmed和我们的SelectedItem进行赋值:
依旧省去了大部分代码,只看与我们有关的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    public class ItemSelectionViewModel : BindableBase, IInteractionRequestAware
{
public string SelectedItem { get; set; }
private void CancelInteraction()
{
_notification.SelectedItem = null;
_notification.Confirmed = false;
FinishInteraction?.Invoke();
}

private void AcceptSelectedItem()
{
_notification.SelectedItem = SelectedItem;
_notification.Confirmed = true;
FinishInteraction?.Invoke();
}

public Action FinishInteraction { get; set; }

private ICustomNotification _notification;

public INotification Notification
{
get { return _notification; }
set { SetProperty(ref _notification, (ICustomNotification)value); }
}
}
}

最后就是在Shell里调用这个客制化弹窗啦,跟之前的就一毛一样了,将INotification或者IConfirmation替换成我们的ICustomNotification,然后在new的时候使用CustomNotification,代码看上去应该是这个样子的:
xaml:

1
2
3
4
5
6
7
<prism:InteractionRequestTrigger SourceObject="{Binding CustomNotificationRequest}">
<prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True">
<prism:PopupWindowAction.WindowContent>
<views:ItemSelectionView />
</prism:PopupWindowAction.WindowContent>
</prism:PopupWindowAction>
</prism:InteractionRequestTrigger>

viewmodel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public InteractionRequest<ICustomNotification> CustomNotificationRequest { get; set; }
public DelegateCommand CustomNotificationCommand { get; set; }

public MainWindowViewModel()
{
CustomNotificationRequest = new InteractionRequest<ICustomNotification>();
CustomNotificationCommand = new DelegateCommand(RaiseCustomInteraction);
}
private void RaiseCustomInteraction()
{
CustomNotificationRequest.Raise(new CustomNotification { Title = "Custom Notification" }, r =>
{
if (r.Confirmed && r.SelectedItem != null)
Title = $"User selected: { r.SelectedItem}";
else
Title = "User cancelled or didn't select an item";
});
}

最后一篇了!我会持续修正之前文章里理解偏颇的地方。谢谢大家!

0x5 MVVM#

[7.1updated]截止到目前,我们看到7.1的更新主要在三个地方

  1. PrismApplication ,并且不再使用Bootstrapper
  2. 更新了unity,现在使用prism.unity作为容易管理
  3. 更新了IModule接口
    下面所有代码片段都更新到7.1,并且不再赘述与6.x的区别

蛤蛤,终于到MVVM了。特别是前面的Module,忒难写,反正大概知道是怎么用就好了,具体怎么个容器,怎么个依赖注入,我也不是很懂,Prism重度依赖容器,哪哪都是,哪哪都是依赖容器注入。

到目前为止,已经知道怎么去设置Region,怎么去关联View,和关联其他Module里的View了。那么接下来就是MVVM啦,★,°:.☆( ̄▽ ̄)/$:.°★

先看Wiki怎么对MVVM定义的:

MVVMModel–view–viewmodel)是一种软件架构模式

MVVM有助于将图形用户界面的开发与业务逻辑后端逻辑(_数据模型_)的开发分离开来,这是通过置标语言或GUI代码实现的。MVVM的_视图模型_是一个值转换器,[1] 这意味着视图模型负责从模型中暴露(转换)数据对象,以便轻松管理和呈现对象。在这方面,视图模型比视图做得更多,并且处理大部分视图的显示逻辑。[1] 视图模型可以实现中介者模式,组织对视图所支持的用例集的后端逻辑的访问。

Dior不Dior?首先他不是WPF专有的,现在很多前端框架都实现了MVVM模式,像Vue,Angular。那MVVM这么火,他到底有什么神奇的地方呢?数据双向绑定数据双向绑定数据双向绑定

我最早在找MVVM框架的时候,其实并不在乎什么解耦,前后端分离,可测试啥的,我只是受够了WinForms前台代码中 ShowDetails和SetModel,后来发现MVVM可以实现双向绑定,数据驱动界面显示,就着了迷(❤´艸`❤)。扯远了,我们来看Prism,怎样实现MVVM的。

ViewModel及定位#

什么是ViewModel,ViewModel在MVVM中充当了什么角色?

ViewModel是对应的View(数据和行为)的抽象,View只是ViewModel的一个消费者,那么还有其他的消费者吗?当然有了,那就是单元测试(Unit Test),这个后面说。ViewModel为View提供数据上下文(DataContext),简单的说,你View需要展示的东西,都在我这里,你需要跟我绑定,包括数据和命令,不然你就是个静态的。

那怎么为View指定ViewModel呢,通常情况下,我们是为控件指定Datacontext,而Prism为我们提供了更简单方式,约定

约定的绑定方式
  • Step1 新建一个Wpf项目,新建两个文件夹Views 和 ViewModels,用来存放View和ViewModel,删掉MainWindow.xaml,并在Views新建一个新 MainWindow窗体当我们的Shell。
    app.xaml.cs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using Prism.Ioc;
using Prism.Unity;
using System.Windows;
using ViewModelLocator.Views;

namespace ViewModelLocator
{



public partial class App : PrismApplication
{
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{

}
}
}
  • Step2 在ViewModels文件夹内新建,一个MainViewModel的类,继承BindableBase,注意,这里是个类(Class)

MainWindowViewModel.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using Prism.Mvvm;

namespace ViewModelLocator.ViewModels
{
public class MainWindowViewModel : BindableBase
{
private string _title = "Prism Unity Application";
public string Title
{
get { return _title; }
set { SetProperty(ref _title, value); }
}

public MainWindowViewModel()
{

}
}
}

  • Step3 修改MainWindow.xaml,覆盖下面的代码:(当前也可以不覆盖,对比发现,我们这里只多了一点东西)
1
2
3
4
5
6
7
8
9
10
11
12
<Window x:Class="ViewModelLocator.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"

Title="{Binding Title}" Height="350" Width="525">
<Grid>
<ContentControl prism:RegionManager.RegionName="ContentRegion" />
</Grid>
</Window>

Tips:数据绑定Title=”{Binding Title}” ,Title是对应ViewModel里的一个公开属性。

运行后发现,窗口的Title正式MainWindowViewModel里Title的值,可是我们并没有为MainWindow指定ViewModel啊,正常的绑定看上去是应该是这样

1
2
3
<UserControl.DataContext>
<vm:NumberChangeLogViewModel />
</UserControl.DataContext>

或者这样

1
2
3
4
<vw:NumberView 
DockPanel.Dock="Top"
DataContext="{Binding Path=Number, Mode=OneTime}"
/>

蛤蛤,刚开始我也很懵逼,可是我爱学习,在Prism的源码Prism.Mvvm.ViewModelLocationProvider中我发现了这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18



static Func<object, Type, object> _defaultViewModelFactoryWithViewParameter;




static Func<Type, Type> _defaultViewTypeToViewModelTypeResolver =
viewType =>
{
var viewName = viewType.FullName;
viewName = viewName.Replace(".Views.", ".ViewModels.");
var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
var suffix = viewName.EndsWith("View") ? "Model" : "ViewModel";
var viewModelName = String.Format(CultureInfo.InvariantCulture, "{0}{1}, {2}", viewName, suffix, viewAssemblyName);
return Type.GetType(viewModelName);
};

是不是豁然开朗?O(∩_∩)O

我们不一样,定制约定

约定就是要来被打破的,有人可能觉得后缀加一个ViewModel实在是LowB,我想改变他,可以不可以?当然阔以啦。

prism为我们提供了一个可重写的ConfigureViewModelLocator的方法来配置ViewModel的定位器,如果你想修改默认的约定为View的名字后面+VM,你可以在app.xaml.cs这样写:

1
2
3
4
5
6
7
8
9
10
11
12
protected override void ConfigureViewModelLocator()
{
base.ConfigureViewModelLocator();

ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) =>
{
var viewName = viewType.FullName;
var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
var viewModelName = $"{viewName}VM, {viewAssemblyName}";
return Type.GetType(viewModelName);
});
}
我就是我,是颜色不一样的烟火

这世界上不乏个性鲜明的人,你们那些约定和打破的约定还不都是一路货色。我就要不一样的,我想跟谁绑在一起就跟谁绑在一起。好,你跟谁好是你的自由,Prism不能限制你,不然你会投诉它不民主。😂

如果你想指定你绑定的ViewModel对象又不想遵循一定的规则,你同样可以在ConfigureViewModelLocator方法中注册绑定,像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected override void ConfigureViewModelLocator()
{
base.ConfigureViewModelLocator();











ViewModelLocationProvider.Register<MainWindow, CustomViewModel>();
}

当然了,在xaml中的

1
prism:ViewModelLocator.AutoWireViewModel="True"

依旧是必不可少的。

从TensorFlow 到 Caffe2:盘点深度学习框架 - allcloud - 博客园

Excerpt

机器之心报道 本文首先介绍GitHub中最受欢迎的开源深度学习框架排名,然后再对其进行系统地对比 下图总结了在GitHub中最受欢迎的开源深度学习框架排名,该排名是基于各大框架在GitHub里的收藏数,这个数据由MitchDeFelice在2017年5月初完成。 TensorFlow 地址:http


机器之心报道

本文首先介绍GitHub中最受欢迎的开源深度学习框架排名,然后再对其进行系统地对比

下图总结了在GitHub中最受欢迎的开源深度学习框架排名,该排名是基于各大框架在GitHub里的收藏数,这个数据由MitchDeFelice在2017年5月初完成。

TensorFlow

地址:https://www.tensorflow.org/

TensorFlow最开始是由谷歌一个称之为DistBeliefV2的库发展而来,它是一个公司内部的深度神经网络库,隶属于谷歌大脑项目。有一些人认为TensorFlow是由Theano彻底重构而来。

谷歌开源TensorFlow后,立即吸引了一大批开发爱好者。TensorFlow可以提供一系列的能力,例如图像识别、手写识别、语音识别、预测以及自然语言处理等。2015年11月9号,TensorFlow在Apache2.0协议下开源发布。

TensorFlow1.0版本已于2017年2月15日发布,这个版本是之前8个版本的优化改进版,其致力于解决Tensorflow之前遇到的一系列问题以及完善一些核心能力。TensorFlow获得成功的因素有:

TensorFlow提供了如下工具:

TensorBoard:对于网络模型和效果来说是一个设计优良的可视化工具。TensorFlowServing:可以保持相同的服务器架构和API,使得部署新算法和实验变得简单。TensorFlowServing提供了与TensorFlow模型开箱即用的整合,但同时还能很容易扩展到其它类型的模型和数据。

TensorFlow编程接口支持Python和C++。随着1.0版本的公布,Java、Go、R和HaskellAPI的alpha版本也将被支持。此外,TensorFlow还可在谷歌云和亚马孙云中运行。

随着0.12版本的发行,TensorFlow将支持Windows7、Windows10和Server2016。由于TensorFlow使用C++Eigen库,所以库可在ARM架构上编译和优化。这也就意味着你可以在各种服务器和移动设备上部署你的训练模型,而无需执行单独的模型解码器或者加载Python解释器。

TensorFlow支持细粒度的网格层,而且允许用户在无需用低级语言实现的情况下构建新的复杂的层类型。子图执行操作允许你在图的任意边缘引入和检索任意数据的结果。这对调试复杂的计算图模型很有帮助。

分布式TensorFlow(DistributedTensorFlow)被加进了0.8版本,它允许模型并行,这意味着模型的不同部分可在不同的并行设备上被训练。

自2016年3月,斯坦福大学、伯克利大学、多伦多大学和Udacity都将这个框架作为一个免费的大规模在线开放课程进行教授。

TensorFlow的缺点如下:

TensorFlow的每个计算流都必须构造为一个静态图,且缺乏符号性循环(symbolicloops),这会带来一些计算困难。没有对视频识别很有用的三维卷积(3-Dconvolution)。尽管TensorFlow现在比起始版本(v0.5)快了58倍,,但在执行性能方面依然落后于竞争对手。

Caffe

地址:http://caffe.berkeleyvision.org/

Caffe是贾扬清的杰作,目前他在FacebookAI平台担任首席工程师。Caffe可能是自2013年底以来第一款主流的工业级深度学习工具包。正因为Caffe优秀的卷积模型,它已经成为计算机视觉界最流行的工具包之一,并在2014年的ImageNet挑战赛中一举夺魁。Caffe遵循BSD2-Clause协议。

Caffe的快速使其完美应用于实验研究和商业部署。Caffe可在英伟达单个K40GPU上每天处理6000万张图像。这大概是1毫秒预测一张图片,4毫秒学习一张图片的速度,而且最新的版本处理速度会更快。

Caffe基于C++,因此可在多种设备上编译。它跨平台运行,并包含Windows端口。Caffe支持C++、Matlab和Python编程接口。Caffe拥有一个庞大的用户社区,人们在其中为被称为「ModelZoo(https://github.com/BVLC/caffe/wiki/Model-Zoo)」的深度网络库做贡献。AlexNet和GoogleNet就是社群用户构建的两个流行网络。

虽然Caffe在视频识别领域是一个流行的深度学习网络,但是Caffe却不能像TensorFlow、CNTK和Theano那样支持细粒度网络层。构建复杂的层类型必须以低级语言完成。由于其遗留架构,Caffe对循环网络和语言建模的支持总体上很薄弱。

Caffe2

地址:https://caffe2.ai/

目前,贾扬清和他在Facebook的团队正在开发新一代框架Caffe2。今年4月18日,Facebook开源了Caffe2。Caffe2与Caffe的区别是什么?Caffe2更注重模块化,在移动端、大规模部署上表现卓越。如同TensorFlow,Caffe2使用C++Eigen库,支持ARM架构。

用一个实用脚本,Caffe上的模型可轻易地被转变到Caffe2上。Caffe设计的选择使得它处理视觉类型的难题时很完美。Caffe2延续了它对视觉类问题的支持,且增加了对自然语言处理、手写识别、时序预测有帮助的RNN和LSTM支持。

期待不久之后能看到Caffe2超越Caffe,就像它宣称的那样在深度学习社区流行。

在本周三英伟达推出Volta架构的第一块加速卡TeslaV100后,Caffe的开发者第一时间展示了TeslaV100在Caffe2上运行ResNet-50的评测。数据显示在新框架和新硬件的配合下,模型每秒钟可以处理4100张图片。

链接:https://caffe2.ai/blog/2017/05/10/caffe2-adds-FP16-training-support.html

CNTK

链接:https://github.com/Microsoft/CNTK/wiki

微软的CNTK(MicrosoftCognitiveToolkit)最初是面向语音识别的框架。CNTK支持RNN和CNN类型的网络模型,从而在处理图像、手写字体和语音识别问题上,它是很好的选择。使用Python或C++编程接口,CNTK支持64位的Linux和Windows系统,在MIT许可证下发布。

与TensorFlow和Theano同样,CNTK使用向量运算符的符号图(symbolicgraph)网络,支持如矩阵加/乘或卷积等向量操作。此外,像TensorFlow和Theano一样,CNTK有丰富的细粒度的网络层构建。构建块(操作)的细粒度使用户不需要使用低层次的语言(如Caffe)就能创建新的复杂的层类型。

CNTK也像Caffe一样基于C++架构,支持跨平台的CPU/GPU部署。CNTK在AzureGPULab上显示出最高效的分布式计算性能。目前,CNTK不支持ARM架构,这限制了其在移动设备上的功能。

MXNet

链接:http://mxnet.io/

MXNet(发音为mix-net)起源于卡内基梅隆大学和华盛顿大学的实验室。MXNet是一个全功能、可编程和可扩展的深度学习框架,支持最先进的深度学习模型。MXNet支持混合编程模型(命令式和声明式编程)和多种编程语言的代码(包括Python、C++、R、Scala、Julia、Matlab和JavaScript)。2017年1月30日,MXNet被列入ApacheIncubator开源项目。

MXNet支持深度学习架构,如卷积神经网络(CNN)、循环神经网络(RNN)和其包含的长短时间记忆网络(LTSM)。该框架为图像、手写文字和语音的识别和预测以及自然语言处理提供了出色的工具。有些人称MXNet是世界上最好的图像分类器。

MXNet具有可扩展的强大技术能力,如GPU并行和内存镜像、快速编程器开发和可移植性。此外,MXNet与ApacheHadoopYARN(一种通用分布式应用程序管理框架)集成,使MXNet成为TensorFlow有力的竞争对手。

MXNet不仅仅只是深度网络框架,它的区别在于支持生成对抗网络(GAN)模型。该模型启发自实验经济学方法的纳什均衡。

Torch

链接:http://torch.ch/

Torch由Facebook的RonanCollobert和SoumithChintala,Twitter的ClementFarabet(现任职于英伟达),以及GoogleDeepMind的KorayKavukcuoglu共同开发。很多科技巨头(如Facebook、Twitter和英伟达)都使用定制版的Torch用于人工智能研究,这大大促进了Torch的开发。Torch是BSD3协议下的开源项目。然而,随着Facebook对Caffe2的研究,以及其对移动设备的支持,Caffe2正成为主要的深度学习框架。

Torch的编程语言为Lua。Lua不是主流语言,在开发人员没有熟练掌握Lua之前,使用Torch很难提高开发的整体生产力。

Torch缺乏TensorFlow的分布式应用程序管理框架,也缺乏MXNet和Deeplearning4J对YARN的支持。缺乏多种编程语言的API也限制了开发人员。

PyTorch

地址:http://pytorch.org/

PyTorch由AdamPaszke、SamGross与SoumithChintala等人牵头开发,其成员来自FacebookFAIR和其他多家实验室。它是一种Python优先的深度学习框架,在今年1月被开源,提供了两种高层面的功能:

使用强大的GPU加速的Tensor计算(类似numpy)

构建于基于tape的autograd系统的深度神经网络

该框架结合了Torch7高效灵活的GPU加速后端库与直观的Python前端,它的特点是快速成形、代码可读和支持最广泛的深度学习模型。如有需要,你可以复用你最喜欢的Python软件包(如numpy、scipy和Cython)来扩展PyTorch。该框架因为其灵活性和速度,在推出以后迅速得到了开发者和研究人员的青睐。随着GitHub上越来越多代码的出现,PyTorch作为新框架缺乏资源的问题已经得以缓解。

Deeplearning4J

地址:https://deeplearning4j.org/

Deeplearning4J(DL4J)是用Java和Scala编写的Apache2.0协议下的开源、分布式神经网络库。DL4J最初由SkyMind公司的AdamGibson开发,是唯一集成了Hadoop和Spark的商业级深度学习网络,并通过Hadoop和Spark协调多个主机线程。DL4J使用Map-Reduce来训练网络,同时依赖其它库来执行大型矩阵操作。

DL4J框架支持任意芯片数的GPU并行运行(对训练过程至关重要),并支持YARN(Hadoop的分布式应用程序管理框架)。DL4J支持多种深度网络架构:RBM、DBN、卷积神经网络(CNN)、循环神经网络(RNN)、RNTN和长短时间记忆网络(LTSM)。DL4J还对矢量化库Canova提供支持。

DL4J使用Java语言实现,本质上比Python快。在用多个GPU解决非平凡图像(non-trivialimage)识别任务时,它的速度与Caffe一样快。该框架在图像识别、欺诈检测和自然语言处理方面的表现出众。

Theano

地址:http://deeplearning.net/software/theano/

Theano由蒙特利尔大学算法学习人工智能实验室(MILA)维护。以Theano的创始人YoshuaBengio为首,该实验室是深度学习研究领域的重要贡献者,拥有约30至40名学生和教师。Theano支持快速开发高效的机器学习算法,在BSD协议下发布。

Theano的架构如同一个黑箱;整个代码库和接口使用Python,其中C/CUDA代码被打包成Python字符串。这使得开发人员很难导航(navigate)、调试和重构。

Theano开创了将符号图用于神经网络编程的趋势。Theano的符号式API支持循环控制(即scan),这使得实现RNN容易且高效。

Theano缺乏分布式应用程序管理框架,只支持一种编程开发语言。Theano是很好的学术研究工具,在单个CPU上运行的效率比TensorFlow更有效。然而,在开发和支持大型分布式应用程序时,使用Theano可能会遇到挑战。

在了解这些深度学习框架的基本内容后,下面我们可以看看它们之间在库资源、建模能力、速度等度量下的对比情况。

这组对比参考了多种公开基准评测,以及我们在图像/语音识别应用时对这些技术的主观印象。此外,你需要注意:

语言

当你开始一个深度学习项目时,你最好使用一个支持你所会语言的框架。比如Caffe(C++)和Torch(Lua)只能支持有限的语言(最近,随着PyTorch的出现,情况有所改观)。所以如果你希望选用上述两个框架,我们建议你事先熟悉C++或Lua语言。相比之下,TensorFlow与MXNet具有丰富的多语言支持,即使你对C++感到陌生也可以使用它们。

教程和资源

目前,各类深度学习框架的教程与可利用的资源在质量和数量上有着显著的不同。Theano,TensorFlow,Torch和MXNet有着很详尽的文档教程,很容易被初学者理解和实现。与此相比,虽然微软的CNTK和英特尔的NervanaNeon也是强大的工具,我们却很少能见到有关它们的新手级资料。此外,在研究过程中,我们发现GitHub社区的参与度不仅可以用于准确地评价不同工具的开发水平,而且还是在搜索StackOverflow或repo的GitIssues时能否快速解决问题的参考性指标。当然,作为谷歌提供的框架,TensorFlow理所当然地在教程,资源,开发者和社区贡献者的数量上遥遥领先。

CNN建模能力

卷积神经网络(CNN)经常被用于图像识别、推荐引擎和自然语言识别等方向的应用。CNN由一组多层的神经网络组成,在运行时会将输入的数据进行预定义分类的评分。CNN也可用于回归分析,例如构成自动驾驶汽车中有关转向角的模型。在横评中,我们评价一种框架的CNN建模能力考虑到以下几个特性:定义模型的机会空间、预构建层的可用性、以及可用于连接这些层的工具和功能。我们发现,Theano,Caffe和MXNet都有很好的CNN建模能力。其中,TensorFlow因为易于建立的InceptionV3模型,Torch因为其丰富的CNN资源——包括易于使用的时间卷积集使得这两种框架在CNN建模能力上脱颖而出。

RNN建模能力

循环神经网络(RNN)常用于语音识别,时间序列预测,图像字幕和其他需要处理顺序信息的任务。由于预建的RNN模型不如CNN数量多,因此,如果你已经有一个RNN深度学习项目,优先考虑旧RNN模型是在哪种框架里实现的最重要。目前,Caffe上的RNN资源最少,而Microsoft的CNTK和Torch有丰富的RNN教程和预构建模型。当然,最流行的TensorFlow中也有一些RNN资源,TFLearn和Keras中更有很多使用TensorFlow的RNN示例。

架构

为在特定框架中构建和训练新模型,易于使用和模块化的前端是至关重要的。TensorFlow,Torch和MXNet都有直观而模块化的架构,让开发相对变得简单。相比之下,我们在Caffe这样的框架上需要进行大量的工作才能创建一个新层。另外我们发现在开发过程中,因为有TensorBoardwebGUI等应用的存在,TensorFlow极易在训练中和训练后进行debug和监控。

速度

Torch和Nervana具有开源卷积神经网络基准测试的最佳性能:

https://github.com/soumith/convnet-benchmarks/blob/master/README.md

Tensorflow的性能在大多数测试中是具有竞争力的,而Caffe和Theano稍稍落后:

https://github.com/tobigithub/tensorflow-deep-learning/wiki/tf-benchmarks

微软声称他们的CNTK在一些RNN训练任务中有最快的速度。

在另一项对比Theano、Torch和TensorFlow的RNN性能的研究中,Theano是其中最快的:

https://arxiv.org/abs/1511.06435

多GPU支持

大多数深度学习应用都需要用到巨量的浮点运算(FLOP)。例如,百度的DeepSpeech识别模型需要10sExaFLOPs用于训练,这是大于10e18的计算量:

https://arxiv.org/abs/1512.02595

考虑到目前英伟达Pascal架构的TitanX等顶级显卡可以每秒执行10e9FLOP:

https://www.nvidia.com/en-us/geforce/products/10series/titan-x-pascal/

因此,假如需要在大型数据集上训练一个新模型——用单GPU机器的话——可能会需要一个星期之久。为了减少构建模型所需的时间,我们需要使用多GPU并联的方式组建自己的机器。幸运的是,上述大部分架构都可以很好地支持多GPU运算。其中,据报道MXNet有着最好的多GPU优化引擎:

http://www.allthingsdistributed.com/2016/11/mxnet-default-framework-deep-learning-aws.html

Keras兼容性

Keras是一个用于快速构建深度学习原型的高级库。我们在实践中发现,它是数据科学家应用深度学习的好帮手。Keras目前支持两种后端框架:TensorFlow与Theano,而且Keras再过不久就会成为TensorFlow的默认API:

http://www.fast.ai/2017/01/03/keras/

尽管如此,Keras的作者表示,这一高级库在未来仍会作为支持多种框架的前端存在:

https://github.com/fchollet/keras/issues/5050#issuecomment-272945570

总结

如果你想要开始深度学习,你应该从评估自己的团队技能和业务需求开始。例如,如果一个以Python为中心的团队想开发图像识别的应用程序,你应该使用TensorFlow,因为它有丰富的资源,较好性能和完整的原型工具。如果一个有Lua能力的团队希望将RNN大规模应用到生产环境中去,他们则会受益于Torch的高速和强大的RNN建模能力。

未来,我们将继续讨论在更大规模的应用中这些框架的表现。这些挑战包括多机并联时的多GPU优化,多种开源库的兼容性,如CMUSphinx和Kaldi等,尽请期待。

正文

    作为一个.Net攻城狮已经4年有余了,一直不温不火,正好近来项目不是很忙,闲得无聊,搞一搞新技术,一方面是打发无聊的时间,一方面也是督促自己该学习辣!身边的大神都转行的转行,加薪的加薪,本人比较懒,只想搞技术 [哭笑] ,也是怀着小小的梦想,做一个系列文章可以和大家一起进步,讨论,希望总阅读数能上1万(已实现2018年12月28日19:41),2万(2019年4月3日19:36),3万(2019年5月28日09:37),4万(2019年7月16日11点00分),5(2019年9月28日23点13分),6 (2019年11月22日18点47分) ,Seven(2019年12月31日18点57分)!,哈哈哈哈



    本系列文章只是对现有的一些技术做一个简单说明或者是引入,只是一个抛砖引玉的作用,主要的还是希望和志同道合的大神们一起切磋武艺。

    系统环境

    windows 10、SQL server 2012、MySql/MSSql/SQLite、Visual Studio 2019、Windows Server 2008 R2

    后端技术:

      * .Net Core 2.2+ 版本 API(因为想单纯搭建前后端分离,因此就选用的API,如果想了解.Net Core MVC,也可以交流)

      * Async和Await 异步编程

      * Repository + Service 仓储模式编程

      * Swagger 前后端文档说明,基于RESTful风格编写接口

      * Cors 简单的跨域解决方案

      * AOP基于切面编程技术

      * Autofac 轻量级IoC和DI依赖注入

      * Vue 本地代理跨域方案,Nginx跨域代理

      * JWT权限验证

      * DI 依赖注入

    数据库技术

      * SqlSugar 轻量级ORM框架,CodeFirst

      * T4 模板生成

      * AutoMapper 自动对象映射

    分布式缓存技术

      * Redis 轻量级分布式缓存

    前端技术

      * Vue 2.0 框架全家桶 Vue2 + VueRouter2 + Webpack + Axios + vue-cli + vuex(@ 指正)

      * ElementUI 基于Vue 2.0的组件库

      * Nuxt.js服务端渲染SSR


    这里再一次说明,仅仅是简单的特别简单的入门使用,如果对于上边的技术,你从来没有听过,或者听过没用过,嗯,你可以简单花点儿时间看一看,但是如果你都已经用过或者有一定的技术,请帮忙监督指正。

(Blog.Core整体系统架构图)

(整个框架用到的知识点的思维导图)


    感谢有两位朋友提供思路,或者说动力,才使我萌发了想写的冲动,特别感谢李大爷,嗯就是哈哈,的默默支持,才使我有了继续写下去的动力。

    

  最近有个项目的数据库使用postgresql,使用原生态的mybatis操作数据,原生态的没什么不好,只不过国内有个tk.mybatis的工具帮助我们做了很多实用的事情,大多数情况下我们需要在原生态mybatis上加工的想法它基本上都已经有很好的实现,这篇将分享安装postgresql,配置tk.mybatis的详细步骤以及在这过程中可能遇到的一些小问题。

  •   安装postgresql,执行下面的命令就可以安装了:

          

apt-get update && apt-get install postgresql

服务端安装好之后我们还需要一个图形界面的客户端pdAdmin,我安装的是Windows版本的postgresql自带的,可以到这个地址找对应的版本。安装成功后默认会创建一个系统用户,一个数据库用户,名称以及密码都是postgres,我们可以新创建用户也可以直接使用这个帐号,反正我这只是测试。安装完成之后,可能会遇到远程访问问题:

  远程连接问题,默认情况下只允许本地连接,要想允许其它客户端连接,我们可以修改它的配置文件,这个文件的目录位于/etc/postgresql/9.5/main,这个目录下有两个文件:
  1:postgresql.conf,这个是服务器相关,里面有一个listen_address的地址,默认只监听本地,我们可以修改它。

    
  2:pg_hba.cof,这个是用户权限相关,里面有一个与连接相关的配置,可以配置成网关模式

    

     成功连接之后,大概是这个样子,我们可以创建数据库,表等对象。

 

  •   mybatis代码生成器,数据库与Model的映射,这类机械的工作应该交给机器来完成,详细使用参考这里

  •   通用mapper,单表的CRUD操作可以抽像出一个公共接口,tk.mybatis提供的通用mapper可以帮助我们解决这类问题。

    • mapper.xml,足够小(只包含字段映射)

复制代码

<mapper namespace=“com.jim.logstashmvc.dao.generated.mapper.ProductMapper”>
<resultMap id=“BaseResultMap” type=“com.jim.logstashmvc.dao.generated.entity.Product”>

<id column=“id” jdbcType=“BIGINT” property=“id” />
<result column=“name” jdbcType=“VARCHAR” property=“name” />
</resultMap>
</mapper>

复制代码

    • mapper,足够简单(只需要继承通过mapper接口)

public interface ProductMapper extends Mapper {
}

  •   插件,这里有分页插件,SQL性能分析插件等,与mybatis集成非常容易。

  

  如何与spring集成?

  • 生成器的集成,可以采用maven方式来运行代码生成器

    • 依赖的包

复制代码

    <dependency\>
        <groupId\>org.mybatis</groupId\>
        <artifactId\>mybatis</artifactId\>
        <version\>${mybatis.version}</version\>
    </dependency\>

    <!-- Spring集成 \-->
    <dependency\>
        <groupId\>org.mybatis</groupId\>
        <artifactId\>mybatis-spring</artifactId\>
        <version\>${mybatis.spring.version}</version\>
    </dependency\>

    <!-- MBG \-->
    <dependency\>
        <groupId\>org.mybatis.generator</groupId\>
        <artifactId\>mybatis-generator-core</artifactId\>
        <version\>${MBG.version}</version\>
        <scope\>compile</scope\>
        <optional\>true</optional\>
    </dependency\>

    <!-- 分页 \-->
    <dependency\>
        <groupId\>com.github.pagehelper</groupId\>
        <artifactId\>pagehelper</artifactId\>
        <version\>${pagehelper.version}</version\>
    </dependency\>

    <!-- 通用Mapper \-->
    <dependency\>
        <groupId\>tk.mybatis</groupId\>
        <artifactId\>mapper</artifactId\>
        <version\>${mapper.version}</version\>
    </dependency\>

    <!-- TkMybatis 会使用到JPA的注解 \-->
    <dependency\>
        <groupId\>javax.persistence</groupId\>
        <artifactId\>persistence-api</artifactId\>
        <version\>1.0</version\>
    </dependency\>

    <dependency\>
        <groupId\>org.postgresql</groupId\>
        <artifactId\>postgresql</artifactId\>
        <version\>9.3-1102-jdbc41</version\>
    </dependency\>

复制代码

    •  配置生成器插件,指定配置文件路径,配置依赖:一个是数据库驱动,一个是通用mapper

复制代码

        <plugin\>
            <groupId\>org.mybatis.generator</groupId\>
            <artifactId\>mybatis-generator-maven-plugin</artifactId\>
            <version\>${MBG.version}</version\>
            <configuration\>
                <configurationFile\>${basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile\>
                <overwrite\>true</overwrite\>
                <verbose\>true</verbose\>
            </configuration\>
            <dependencies\>
                <dependency\>
                    <groupId\>org.postgresql</groupId\>
                    <artifactId\>postgresql</artifactId\>
                    <version\>9.3-1102-jdbc41</version\>
                </dependency\>

                <dependency\>
                    <groupId\>tk.mybatis</groupId\>
                    <artifactId\>mapper</artifactId\>
                    <version\>${mapper.version}</version\>
                </dependency\>

            </dependencies\>
        </plugin\>

复制代码

  •   生成器配置文件
    •  配置数据库连接
    •  配置生成的model,mapper以及mapper.xml的存放路径
    •  配置需要生成的表信息

          注意下targetRuntime,这里采用的是MyBatis3Simple,它的默认选项是MyBatis3。如果采用通用mapper,我们在spring扫描接口时可以这样写。   

<bean class=“tk.mybatis.spring.mapper.MapperScannerConfigurer”>
<property name=“sqlSessionFactoryBeanName” value=“jimSqlSessionFactory”/>
<property name=“basePackage” value=“com.jim.logstashmvc.dao.generated.mapper”/>
</bean>

        如果是MyBatis3,生成的mapper.xml格式会复杂很多,我之前遇到过这样的问题:使用MyBatis3生成的mapper.xml然后错误 的配置了MapperScannerConfigurer为下面通用mapper模式,提示我的错误如下,原因可以认定是配置问题(不是某个mapper.xml中的id重复问题) ,后续再研究下非通用mapper的配置。

Caused by: java.lang.IllegalArgumentException: Mapped Statements collection already contains value for com.jim.logstashmvc.dao.generated.mapper.ProductMapper.selectByExample
at org.apache.ibatis.session.Configuration$StrictMap.put(Configuration.java:837)
at org.apache.ibatis.session.Configuration$StrictMap.put(Configuration.java:809)

 

  上面的报错信息已经找到,原因是因为采用了MyBatis3方式生成Mapper,但同时在配置文件中错误的增加了通用Mapper的插件,它会在Mapper接口上增加一个Mapper的继承,而一旦继承了这个通过接口,它里面包含的方法Id与MyBatis3生成的方法Id正好重复,导致了在编译时提示上面的错误。

 

   使用了通用Mapper之后,有个缺点就是使用Example时没有强类型的方法了,比如不能这样:

    ProductExample example =new ProductExample();
    ProductExample.Criteria criteria\=example.createCriteria(); if (product.getId() != null) {
        criteria.andIdEqualTo(product.getId());
    }

  而只能这样写,字段名称需要以字符串的形式进行指定。这种方式非常适合于动态构建查询,比如:列表页的动态条件搜索

    Example example = new Example(Product.class);
    Example.Criteria criteria \= example.createCriteria(); if (product.getId() != null) {
        criteria.andEqualTo("id", product.getId());
    }

     我们之前的项目为了能够使用动态条件构建,又想使用强类型的Example进行简单的查询,我们扩展了targetRuntime,让其生成的Mapper即继承自Map又生成了具体的方法,只不过生成的具体方法在名称上做了调整,在名称中间增加了一个独有的占有符,默认MBG生成的查询方法名称是selectByExample,这里我们另外生成一个selectByConcreteExample。这样做也是有代价的,生成的mapper.xml也不再只包含实体映射,mapper接口也不再只是继承的近似空接口了,具体怎么用仁者见仁吧。

复制代码

public interface ImageMapper extends PartyInterface, RowBoundsMapper, BaseMapper, ExampleMapper { int countByConcreteExample(ImageExample example); int deleteByConcreteExample(ImageExample example);

List<Image> selectByConcreteExample(ImageExample example); int updateByConcreteExampleSelective(@Param("record") Image record, @Param("example") ImageExample example); int updateByConcreteExample(@Param("record") Image record, @Param("example") ImageExample example);

}

复制代码

     生成器的配置详细如下:

复制代码

<generatorConfiguration>
<properties resource=“config.properties”/>
<context id=“jim” targetRuntime=“MyBatis3Simple” defaultModelType=“flat”>
<property name=“beginningDelimiter” value=“`“/>
<property name=“endingDelimiter” value=“`“/>
<plugin type=“${mapper.plugin}”>
<property name=“mappers” value=“${mapper.Mapper}”/>
</plugin>
<jdbcConnection driverClass=“${jdbc.driverClass}” connectionURL=“${jdbc.url}” userId=“${jdbc.username}” password=“${jdbc.password}”>
</jdbcConnection>
<javaModelGenerator targetPackage=“${targetModelPackage}” targetProject=“${targetJavaProject}”/>
<sqlMapGenerator targetPackage=“mapper” targetProject=“${targetResourcesProject}”/>
<javaClientGenerator targetPackage=“${targetMapperPackage}” targetProject=“${targetJavaProject}” type=“XMLMAPPER”>
</javaClientGenerator>
<table tableName=“product” domainObjectName=“Product”></table>
</context>
</generatorConfiguration>

复制代码

  •  配置maven运行参数,如下图所示即可。

       

  • mybatis的集成,主要是配置连接池信息,插件,mapper扫描等信息。

复制代码

<bean id=“jimDataSource” class=“org.apache.commons.dbcp.BasicDataSource”>
<property name=“driverClassName” value=“${jdbc.driverClass}”/>
<property name=“url” value=“${jdbc.url}”/>
<property name=“username” value=“${jdbc.username}”/>
<property name=“password” value=“${jdbc.password}”/>

    <property name\="initialSize" value\="5"/>
    <property name\="minIdle" value\="10"/>
    <property name\="maxWait" value\="60000"/>
    <property name\="timeBetweenEvictionRunsMillis" value\="60000"/>
    <property name\="minEvictableIdleTimeMillis" value\="3600000"/>
    <property name\="validationQuery" value\="SELECT 1"/>
    <property name\="testWhileIdle" value\="true"/>
    <property name\="testOnBorrow" value\="false"/>
    <property name\="testOnReturn" value\="false"/>
</bean\>

<bean id\="jimSqlSessionFactory" class\="org.mybatis.spring.SqlSessionFactoryBean"\>
    <property name\="dataSource" ref\="jimDataSource"/>
    <property name\="mapperLocations" value\="classpath:mapper/\*.xml"/>
    <property name\="typeAliasesPackage" value\="com.jim.logstashmvc.dao.generated.entity"/>
    <property name\="plugins"\>
        <array\>
            <bean class\="com.github.pagehelper.PageHelper"\>
                <property name\="properties"\>
                    <value\> dialect=postgresql
                    reasonable=true
                    supportMethodsArguments=true
                    returnPageInfo=check
                    params=count=countSql </value\>

                </property\>
            </bean\>
        </array\>
    </property\>

</bean\>

<bean class\="tk.mybatis.spring.mapper.MapperScannerConfigurer"\>
    <property name\="sqlSessionFactoryBeanName" value\="jimSqlSessionFactory"/>
    <property name\="basePackage" value\="com.jim.logstashmvc.dao.generated.mapper"/>
</bean\>

复制代码

  •   通用mapper的用法:
    •  mapper,所有生成的mapper就继承于Mapper,默认持有通用mapper所有接口,包含CRUD常见操作
    •  IService,通用mapper接口的定义,我们可以根据自己的业务修改此接口

复制代码

@Service public interface IService {

T selectByKey(Object key); int save(T entity); int delete(Object key); int updateAll(T entity); int updateNotNull(T entity);

List<T> selectByExample(Object example); //TODO 其他...

}

复制代码

    •  BaseService,通用mapper的实现类

复制代码

public abstract class BaseService implements IService {

@Autowired protected Mapper<T> mapper; public Mapper<T> getMapper() { return mapper;
}

@Override public T selectByKey(Object key) { return mapper.selectByPrimaryKey(key);
} public int save(T entity) { return mapper.insert(entity);
} public int delete(Object key) { return mapper.deleteByPrimaryKey(key);
} public int updateAll(T entity) { return mapper.updateByPrimaryKey(entity);
} public int updateNotNull(T entity) { return mapper.updateByPrimaryKeySelective(entity);
} public List<T> selectByExample(Object example) { return mapper.selectByExample(example);
} //TODO 其他...

}

复制代码

    •   具体服务类

复制代码

@Service public class ProductServiceImpl extends BaseService implements ProductService {
@Override public List selectByProduct(Product product, int page, int rows) {
Example example = new Example(Product.class);
Example.Criteria criteria = example.createCriteria(); if(!StringUtils.isBlank(product.getName())){
criteria.andEqualTo(“name”,product.getName());
} if (product.getId() != null) {
criteria.andEqualTo(“id”, product.getId());
}
PageHelper.startPage(page, rows); return selectByExample(example);

}

}

复制代码

安装postgresql并且成功远程连接,集成MBG生成mapper以及model,然后将mybatis与spring集成,最后通过通用mapper中联起来就达到了我们的目的:通过少量的代码完成大部分的工作,重复劳动交给工具完成。但通用mapper有它的优点也就有它的缺点,需要根据项目环境来平衡,个人感觉利大于弊。

   本文引用:
   1:http://www.mybatis.tk/
   2:https://github.com/abel533/Mybatis-Spring

从零开始学习AvalonDock(1) - skystalker - 博客园

Excerpt

由于个人需要,时不时得开发上位机程序,之前用过WIN API和C# WIN FORM,但都是开发一两个程序就不用了。这次又需要弄一个上位机,为了不花大量精力来弄界面,经过两天业余时间的搜索资料,决定选用WPF。这个程序需要有类似VS2008和KEIL等软件的DOCKING PANEL功能,选用了开源


由于个人需要,时不时得开发上位机程序,之前用过WIN API和C# WIN FORM,但都是开发一两个程序就不用了。这次又需要弄一个上位机,为了不花大量精力来弄界面,经过两天业余时间的搜索资料,决定选用WPF。

这个程序需要有类似VS2008和KEIL等软件的DOCKING PANEL功能,选用了开源的控件AVALONDOCK。

本人非专职上位机开发人员,只求用最快的速度达到需求,只把WPF的概念大约了解了一下,不打算单独研究XAML的语法。先建立一个DEMO感受一下,获得一些成就感是明智的。

新建一个空的工程,在references中添加avalondock的DLL,然后把XMAL文件按如下修改

View Code

然后增加

View Code

也可以不要上面那部分代码,后台代码修改成如下,职业习惯,我更喜欢在后台生成窗口。

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<span>using</span><span> System;
</span><span>using</span><span> System.Collections.Generic;
</span><span>using</span><span> System.Linq;
</span><span>using</span><span> System.Text;
</span><span>using</span><span> System.Windows;
</span><span>using</span><span> System.Windows.Controls;
</span><span>using</span><span> System.Windows.Data;
</span><span>using</span><span> System.Windows.Documents;
</span><span>using</span><span> System.Windows.Input;
</span><span>using</span><span> System.Windows.Media;
</span><span>using</span><span> System.Windows.Media.Imaging;
</span><span>using</span><span> System.Windows.Navigation;
</span><span>using</span><span> System.Windows.Shapes;
</span><span>using</span><span> AvalonDock;

</span><span>namespace</span><span> WpfApplication4
{
</span><span>///</span> <span>&lt;summary&gt;</span>
<span>///</span><span> Interaction logic for MainWindow.xaml
</span><span>///</span> <span>&lt;/summary&gt;</span>
<span>public</span> <span>partial</span> <span>class</span><span> MainWindow : Window
{
</span><span>public</span><span> MainWindow()
{
InitializeComponent();

</span><span>var</span> resPanel = <span>new</span> ResizingPanel() { Orientation =<span> Orientation.Horizontal };
</span><span>var</span> dockPane = <span>new</span><span> DockablePane() { };
</span><span>var</span> documentPane = <span>new</span><span> DocumentPane() { };

dockPane.Items.Add(</span><span>new</span><span> DockableContent()
{
Name </span>= <span>"</span><span>classesContent</span><span>"</span><span>,
Title </span>= <span>"</span><span>Classes</span><span>"</span><span>
});

documentPane.Items.Add(</span><span>new</span><span> DocumentContent()
{
Title </span>= <span>"</span><span>My Document!</span><span>"</span><span>
});

resPanel.Children.Add(dockPane);
resPanel.Children.Add(documentPane);
dockManager.Content </span>=<span> resPanel;
}
}
}</span>

复制代码

运行一下就会出来一个具有浮动和自动停靠等功能的DEMO程序。

以上程序来源:http://avalondock.codeplex.com/wikipage?title=GettingStarted&referringTitle=Documentation

从零开始建立 EMQX MQTT 服务器的 K8S 集群 | EMQ

Excerpt

本文将从零开始部署一个 EMQX MQTT 服务器的 K8S 集群,并分析部署中的细节与技巧,方便用户在实际部署中灵活使用。


EMQX Team 提供了 Helm chart 方便用户在 kubernetes 集群上一键部署 EMQX MQTT 服务器, 这是 EMQX Team 最推荐的在 kubernetes 或 k3s 集群上部署 EMQX MQTT 服务器的方法。 本文将使用手写 yaml 文件的方法从零开始部署一个 EMQX MQTT 服务器的 K8S 集群, 分析部署中的细节与技巧,方便用户在实际部署中灵活使用。

阅读本文需要用户了解 kubernetes 的基本概念,并有一个可操作的 kubernetes 集群。

使用 Pod 直接部署 EMQX Broker

在Kubernetes中,最小的管理元素不是一个个独立的容器,而是 Pod,Pod 是 Kubernetes 应用程序的基本执行单元,即它是 Kubernetes 对象模型中创建或部署的最小和最简单的单元。Pod 表示在 集群 上运行的进程。

EMQX Broker 在 docker hub 上提供了镜像, 因此可以很方便的在单个的 pod 上部署 EMQX Broker,使用 kubectl run 命令创建一个运行着 EMQX Broker 的 Pod:

1
2
$ kubectl run emqx --image=emqx/emqx:v4.1-rc.1  --generator=run-pod/v1
pod/emqx created

查看 EMQX Broker 的状态:

1
2
3
4
5
6
7
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE
emqx 1/1 Running 0 3m13s

$ kubectl exec emqx
Node 'emqx@192.168.77.108' is started
emqx 4.1-rc.1 is running

删除 Pod:

1
2
$ kubectl delete pods emqx
pod "emqx" deleted

Pod 并不是被设计成一个持久化的资源,它不会在调度失败,节点崩溃,或者其他回收中(比如因为资源的缺乏,或者其他的维护中)幸存下来,因此,还需要一个控制器来管理 Pod。

使用 Deoloyment 部署 Pod

Deployment 为 Pod 和 ReplicaSet 提供了一个声明式定义(declarative)方法,用来替代以前的ReplicationController 来方便的管理应用。典型的应用场景包括:

  • 定义Deployment来创建Pod和ReplicaSet
  • 滚动升级和回滚应用
  • 扩容和缩容
  • 暂停和继续Deployment

使用 Deployment 部署一个 EMQX Broker Pod:

  • 定义 Deployment:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    $ cat deployment.yaml

    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: emqx-deployment
    labels:
    app: emqx
    spec:
    replicas: 1
    selector:
    matchLabels:
    app: emqx
    template:
    metadata:
    labels:
    app: emqx
    spec:
    containers:
    - name: emqx
    image: emqx/emqx:v4.1-rc.1
    ports:
    - name: mqtt
    containerPort: 1883
    - name: mqttssl
    containerPort: 8883
    - name: mgmt
    containerPort: 8081
    - name: ws
    containerPort: 8083
    - name: wss
    containerPort: 8084
    - name: dashboard
    containerPort: 18083
  • 部署 Deployment:

    1
    2
    $  kubectl apply -f deployment.yaml
    deployment.apps/emqx-deployment created
  • 查看部署情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    $ kubectl get deployment
    NAME READY UP-TO-DATE AVAILABLE AGE
    deployment.apps/emqx-deployment 3/3 3 3 74s

    $ kubectl get pods
    NAME READY STATUS RESTARTS AGE
    pod/emqx-deployment-7c44dbd68-8j77l 1/1 Running 0 74s

    $ kubectl exec pod/emqx-deployment-7c44dbd68-8j77l
    Node 'emqx-deployment-7c44dbd68-8j77l@192.168.77.117' is started
    emqx 4.1-rc.1 is running
  • 尝试手动删除 Pod

    1
    2
    3
    4
    5
    6
    $ kubectl delete pods emqx-deployment-7c44dbd68-8j77l
    pod "emqx-deployment-7c44dbd68-8j77l" deleted

    $ kubectl get pods
    NAME READY STATUS RESTARTS AGE
    emqx-deployment-68fcb4bfd6-2nhh6 1/1 Running 0 59s

    输出结果表明成功用 Deployment 部署了 EMQX Broker Pod,即使是此 Pod 被意外终止,Deployment 也会重新创建一个新的 Pod。

使用 Services 公开 EMQX Broker Pod 服务

Kubernetes Pods 是有生命周期的。他们可以被创建,而且销毁不会再启动。 如果使用 Deployment 来运行应用程序,则它可以动态创建和销毁 Pod。

每个 Pod 都有自己的 IP 地址,但是在 Deployment 中,在同一时刻运行的 Pod 集合可能与稍后运行该应用程序的 Pod 集合不同。

这导致了一个问题:如果使用 EMQX Broker Pod 为 MQTT 客户端提供服务,那么客户端应该如何如何找出并跟踪要连接的 IP 地址,以便客户端使用 EMQX Broker 服务呢?

答案是:Service

Service 是将运行在一组 Pods 上的应用程序公开为网络服务的抽象方法。

使用 Service 将 EMQX Broker Pod 公开为网络服务:

  • 定义 Service:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    $cat service.yaml

    apiVersion: v1
    kind: Service
    metadata:
    name: emqx-service
    spec:
    selector:
    app: emqx
    ports:
    - name: mqtt
    port: 1883
    protocol: TCP
    targetPort: mqtt
    - name: mqttssl
    port: 8883
    protocol: TCP
    targetPort: mqttssl
    - name: mgmt
    port: 8081
    protocol: TCP
    targetPort: mgmt
    - name: ws
    port: 8083
    protocol: TCP
    targetPort: ws
    - name: wss
    port: 8084
    protocol: TCP
    targetPort: wss
    - name: dashboard
    port: 18083
    protocol: TCP
    targetPort: dashboard
  • 部署 Service:

    1
    2
    $ kubectl apply -f service.yaml
    service/emqx-service created
  • 查看部署情况

    1
    2
    3
    $ kubectl get svc
    NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
    emqx-service ClusterIP 10.96.54.205 <none> 1883/TCP,8883/TCP,8081/TCP,8083/TCP,8084/TCP,18083/TCP 58s
  • 使用 Service 提供的 IP 查看 EMQX Broker 的 API

    1
    2
    3
    $ curl 10.96.54.205:8081/status
    Node emqx-deployment-68fcb4bfd6-2nhh6@192.168.77.120 is started
    emqx is running

至此,单个 EMQX Broker 节点在 kubernetes 上部署完毕,通过 Deployment 管理 EMQX Broker Pod,通过 Service 将 EMQX Broker 服务暴露出去。

通过 kubernetes 自动集群 EMQX MQTT 服务器

上文中通过 Deployment 部署了单个的 EMQX Broker Pod,通过 Deployment 扩展 Pod 的数量是极为方便的,执行 kubectl scale deployment ${deployment_name} --replicas ${numer} 命令即可扩展 Pod 的数量,下面将 EMQX Broker Pod 扩展为 3 个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ kubectl scale deployment emqx-deployment --replicas 3
deployment.apps/emqx-deployment scaled

$ kubectl get pods
NAME READY STATUS RESTARTS AGE
emqx-deployment-68fcb4bfd6-2nhh6 1/1 Running 0 18m
emqx-deployment-68fcb4bfd6-mpvch 1/1 Running 0 6s
emqx-deployment-68fcb4bfd6-mx55q 1/1 Running 0 6s

$ kubectl exec emqx-deployment-68fcb4bfd6-2nhh6 -- emqx_ctl status
Node 'emqx-deployment-68fcb4bfd6-2nhh6@192.168.77.120' is started
emqx 4.1-rc.1 is running

$ kubectl exec emqx-deployment-68fcb4bfd6-2nhh6 -- emqx_ctl cluster status
Cluster status: #{running_nodes =>
['emqx-deployment-68fcb4bfd6-2nhh6@192.168.77.120'],
stopped_nodes => []}

可以看到 EMQX Broker Pod 的数量被扩展为 3 个,但是每个 Pod 都是独立的,并没有集群,接下来尝试通过 kubernetes 自动集群 EMQX Broker Pod。

修改 EMQX Broker 的配置

查看 EMQX Broker 文档中关于自动集群的内容,可以看到需要修改 EMQX Broker 的配置:

1
2
3
4
5
cluster.discovery = kubernetes
cluster.kubernetes.apiserver = http://10.110.111.204:8080
cluster.kubernetes.service_name = ekka
cluster.kubernetes.address_type = ip
cluster.kubernetes.app_name = ekka

其中 cluster.kubernetes.apiserver 为 kubernetes apiserver 的地址,可以通过 kubectl cluster-info 命令获取,cluster.kubernetes.service_name 为上文中 Service 的 name, cluster.kubernetes.app_name 为 EMQX Broker 的 node.name@ 符号之前的部分,所以还需要将集群中 EMQX Broker 设置为统一的 node.name 的前缀。

EMQX Broker 的 docker 镜像提供了通过环境变量修改配置的功能,具体可以查看 docker hubGithub

  • 修改 Deployment 的 yaml 文件,增加环境变量:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    $ cat deployment.yaml

    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: emqx-deployment
    labels:
    app: emqx
    spec:
    replicas: 3
    selector:
    matchLabels:
    app: emqx
    template:
    metadata:
    labels:
    app: emqx
    spec:
    containers:
    - name: emqx
    image: emqx/emqx:v4.1-rc.1
    ports:
    - name: mqtt
    containerPort: 1883
    - name: mqttssl
    containerPort: 8883
    - name: mgmt
    containerPort: 8081
    - name: ws
    containerPort: 8083
    - name: wss
    containerPort: 8084
    - name: dashboard
    containerPort: 18083
    env:
    - name: EMQX_NAME
    value: emqx
    - name: EMQX_CLUSTER__DISCOVERY
    value: k8s
    - name: EMQX_CLUSTER__K8S__APP_NAME
    value: emqx
    - name: EMQX_CLUSTER__K8S__SERVICE_NAME
    value: emqx-service
    - name: EMQX_CLUSTER__K8S__APISERVER
    value: "https://kubernetes.default.svc:443"
    - name: EMQX_CLUSTER__K8S__NAMESPACE
    value: default

    因为 `kubectl scale deployment ${deployment_name} --replicas ${numer} 命令不会修改 yaml 文件,所以修改 yaml 时需要设置 spec.replicas: 3

    Pod 中内建 kubernetes 的 DNS 规则,所以 https://kubernetes.default.svc:443 会被解析为 kubernetes apiserver 的地址。

  • 删除之前的 Deployment,重新部署:

    1
    2
    3
    4
    5
    $ kubectl delete deployment emqx-deployment
    deployment.apps "emqx-deployment" deleted

    $ kubectl apply -f deployment.yaml
    deployment.apps/emqx-deployment created

赋予 Pod 访问 kubernetes apiserver 的权限

上文部署 Deployment 之后,查看 EMQX Broker 的状态,可以看到 EMQX Broker 虽然成功启动了,但是依然没有集群成功,查看 EMQX Broker Pod 的 log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
emqx-deployment-5c8cfc4d75-67lmt 1/1 Running 0 5s
emqx-deployment-5c8cfc4d75-r6jgb 1/1 Running 0 5s
emqx-deployment-5c8cfc4d75-wv2hj 1/1 Running 0 5s

$ kubectl exec emqx-deployment-5c8cfc4d75-67lmt -- emqx_ctl status
Node 'emqx@192.168.87.150' is started
emqx 4.1-rc.1 is running

$ kubectl exec emqx-deployment-5c8cfc4d75-67lmt -- emqx_ctl cluster status
Cluster status: #{running_nodes => ['emqx@192.168.87.150'],
stopped_nodes => []}

$ kubectl logs emqx-deployment-76f6895c46-4684f

···
(emqx@192.168.87.150)1> 2020-05-20 01:48:39.726 [error] Ekka(AutoCluster): Discovery error: {403,
"{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"endpoints \\\"emqx-service\\\" is forbidden: User \\\"system:serviceaccount:default:default\\\" cannot get resource \\\"endpoints\\\" in API group \\\"\\\" in the namespace \\\"default\\\"\",\"reason\":\"Forbidden\",\"details\":{\"name\":\"emqx-service\",\"kind\":\"endpoints\"},\"code\":403}\n"}
···

Pod 因为权限问题在访问 kubernetes apiserver 的时候被拒绝,返回 HTTP 403,所以集群失败。

普通 Pod 是无法访问 kubernetes apiserver 的,解决这个问题有两种方法,一种是开放 kubernetes apiserver 的 http 接口,但是这种方法存在一定的安全隐患,另外一种是通过 ServiceAccount、Role 和 RoleBinding 配置 RBAC 鉴权。

  • 定义 ServiceAccount、Role 和 RoleBinding:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    $ cat rbac.yaml

    apiVersion: v1
    kind: ServiceAccount
    metadata:
    namespace: default
    name: emqx
    ---
    kind: Role
    apiVersion: rbac.authorization.kubernetes.io/v1beta1
    metadata:
    namespace: default
    name: emqx
    rules:
    - apiGroups:
    - ""
    resources:
    - endpoints
    verbs:
    - get
    - watch
    - list
    ---
    kind: RoleBinding
    apiVersion: rbac.authorization.kubernetes.io/v1beta1
    metadata:
    namespace: default
    name: emqx
    subjects:
    - kind: ServiceAccount
    name: emqx
    namespace: default
    roleRef:
    kind: Role
    name: emqx
    apiGroup: rbac.authorization.kubernetes.io
  • 部署相应的资源:

    1
    2
    3
    4
    $ kubectl apply -f rbac.yaml
    serviceaccount/emqx created
    role.rbac.authorization.kubernetes.io/emqx created
    rolebinding.rbac.authorization.kubernetes.io/emqx created
  • 修改 Deployment 的 yaml 文件,增加 spec.template.spec.serviceAccountName,并重新部署:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    $cat deployment.yaml

    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: emqx-deployment
    labels:
    app: emqx
    spec:
    replicas: 3
    selector:
    matchLabels:
    app: emqx
    template:
    metadata:
    labels:
    app: emqx
    spec:
    serviceAccountName: emqx
    containers:
    - name: emqx
    image: emqx/emqx:v4.1-rc.1
    ports:
    - name: mqtt
    containerPort: 1883
    - name: mqttssl
    containerPort: 8883
    - name: mgmt
    containerPort: 8081
    - name: ws
    containerPort: 8083
    - name: wss
    containerPort: 8084
    - name: dashboard
    containerPort: 18083
    env:
    - name: EMQX_NAME
    value: emqx
    - name: EMQX_CLUSTER__DISCOVERY
    value: kubernetes
    - name: EMQX_CLUSTER__K8S__APP_NAME
    value: emqx
    - name: EMQX_CLUSTER__K8S__SERVICE_NAME
    value: emqx-service
    - name: EMQX_CLUSTER__K8S__APISERVER
    value: "https://kubernetes.default.svc:443"
    - name: EMQX_CLUSTER__K8S__NAMESPACE
    value: default

    $ kubectl delete deployment emqx-deployment
    deployment.apps "emqx-deployment" deleted

    $ kubectl apply -f deployment.yaml
    deployment.apps/emqx-deployment created
  • 查看状态:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    $ kubectl get pods
    NAME READY STATUS RESTARTS AGE
    emqx-deployment-6b854486c-dhd7p 1/1 Running 0 10s
    emqx-deployment-6b854486c-psv2r 1/1 Running 0 10s
    emqx-deployment-6b854486c-tdzld 1/1 Running 0 10s

    $ kubectl exec emqx-deployment-6b854486c-dhd7p
    Node 'emqx@192.168.77.92' is started
    emqx 4.1-rc.1 is running

    $ kubectl exec emqx-deployment-6b854486c-dhd7p
    Cluster status: #{running_nodes =>
    ['emqx@192.168.77.115','emqx@192.168.77.92',
    'emqx@192.168.87.157'],
    stopped_nodes => []}
  • 中止一个 Pod:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    $ kubectl delete pods emqx-deployment-6b854486c-dhd7p
    pod "emqx-deployment-6b854486c-dhd7p" deleted

    $ kubectl get pods
    NAME READY STATUS RESTARTS AGE
    emqx-deployment-6b854486c-846v7 1/1 Running 0 56s
    emqx-deployment-6b854486c-psv2r 1/1 Running 0 3m50s
    emqx-deployment-6b854486c-tdzld 1/1 Running 0 3m50s

    $ kubectl exec emqx-deployment-6b854486c-846v7 -- emqx_ctl cluster status
    Cluster status: #{running_nodes =>
    ['emqx@192.168.77.115','emqx@192.168.77.84',
    'emqx@192.168.87.157'],
    stopped_nodes => ['emqx@192.168.77.92']}

    输出结果表明 EMQX Broker 会正确的显示已经停掉的 Pod,并将 Deployment 新建的 Pod 加入集群。

至此,EMQX Broker 在 kubernetes 上成功建立集群。

持久化 EMQX Broker 集群

上文中使用的 Deployment 来管理 Pod,但是 Pod 的网络是不停变动的,而且当 Pod 被销毁重建时,储存在 EMQX Broker 的数据和配置也就随之消失了,这在生产中是不能接受的,接下来尝试把 EMQX Broker 的集群持久化,即使 Pod 被销毁重建,EMQX Broker 的数据依然可以保存下来。

ConfigMap

ConfigMap 是 configMap 是一种 API 对象,用来将非机密性的数据保存到健值对中。使用时可以用作环境变量、命令行参数或者存储卷中的配置文件。

ConfigMap 将您的环境配置信息和 容器镜像 解耦,便于应用配置的修改。

ConfigMap 并不提供保密或者加密功能。如果你想存储的数据是机密的,请使用 Secret ,或者使用其他第三方工具来保证你的数据的私密性,而不是用 ConfigMap。

接下来使用 ConfigMap 记录 EMQX Broker 的配置,并将它们以环境变量的方式导入到 Deployment 中。

  • 定义 Configmap,并部署:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    $cat configmap.yaml

    apiVersion: v1
    kind: ConfigMap
    metadata:
    name: emqx-config
    data:
    EMQX_CLUSTER__K8S__ADDRESS_TYPE: "hostname"
    EMQX_CLUSTER__K8S__APISERVER: "https://kubernetes.default.svc:443"
    EMQX_CLUSTER__K8S__SUFFIX: "svc.cluster.local"

    $ kubectl apply -f configmap.yaml
    configmap/emqx-config created
  • 配置 Deployment 来使用 Configmap

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    $cat deployment.yaml

    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: emqx-deployment
    labels:
    app: emqx
    spec:
    replicas: 3
    selector:
    matchLabels:
    app: emqx
    template:
    metadata:
    labels:
    app: emqx
    spec:
    serviceAccountName: emqx
    containers:
    - name: emqx
    image: emqx/emqx:v4.1-rc.1
    ports:
    - name: mqtt
    containerPort: 1883
    - name: mqttssl
    containerPort: 8883
    - name: mgmt
    containerPort: 8081
    - name: ws
    containerPort: 8083
    - name: wss
    containerPort: 8084
    - name: dashboard
    containerPort: 18083
    envFrom:
    - configMapRef:
    name: emqx-config
  • 重新部署 Deployment,查看状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    $ kubectl delete -f deployment.yaml
    deployment.apps "emqx-deployment" deleted

    $ kubectl apply -f deployment.yaml
    deployment.apps/emqx-deployment created

    $ kubectl get pods
    NAME READY STATUS RESTARTS AGE
    emqx-deployment-5c7696b5d7-k9lzj 1/1 Running 0 3s
    emqx-deployment-5c7696b5d7-mdwkt 1/1 Running 0 3s
    emqx-deployment-5c7696b5d7-z57z7 1/1 Running 0 3s

    $ kubectl exec emqx-deployment-5c7696b5d7-k9lzj
    Node 'emqx@192.168.87.149' is started
    emqx 4.1-rc.1 is running

    $ kubectl exec emqx-deployment-5c7696b5d7-k9lzj
    Cluster status: #{running_nodes =>
    ['emqx@192.168.77.106','emqx@192.168.77.107',
    'emqx@192.168.87.149'],
    stopped_nodes => []}

EMQX Broker 的配置文件已经解耦到 Configmap 中了,如果有需要,可以自由的配置一个或多个 Configmap,并把它们作为环境变量或是文件引入到 Pod 内。

StatefulSet

StatefulSet 是为了解决有状态服务的问题(对应 Deployments 和 ReplicaSets 是为无状态服务而设计),其应用场景包括

  • 稳定的持久化存储,即 Pod 重新调度后还是能访问到相同的持久化数据,基于 PVC 来实现
  • 稳定的网络标志,即 Pod 重新调度后其 PodName 和 HostName 不变,基于 Headless Service(即没有Cluster IP的Service)来实现
  • 有序部署,有序扩展,即 Pod 是有顺序的,在部署或者扩展的时候要依据定义的顺序依次依次进行(即从0到N-1,在下一个Pod运行之前所有之前的 Pod 必须都是 Running 和 Ready 状态),基于 init containers 来实现
  • 有序收缩,有序删除(即从N-1到0)

从上面的应用场景可以发现,StatefulSet由以下几个部分组成:

  • 用于定义网络标志(DNS domain)的 Headless Service
  • 用于创建 PersistentVolumes 的 volumeClaimTemplates
  • 定义具体应用的 StatefulSet

StatefulSet 中每个 Pod 的 DNS 格式为 statefulSetName-{0..N-1}.serviceName.namespace.svc.cluster.local ,其中

  • serviceName 为 Headless Service 的名字
  • 0..N-1 为 Pod 所在的序号,从 0 开始到 N-1
  • statefulSetName 为StatefulSet的名字
  • namespace 为服务所在的 namespace,Headless Servic 和 StatefulSet 必须在相同的 namespace
  • .cluster.local 为 Cluster Domain

接下来使用 StatefulSet 代替 Deployment 来管理 Pod。

  • 删除 Deployment:

    1
    2
    $ kubectl delete deployment emqx-deployment
    deployment.apps "emqx-deployment" deleted
  • 定义 StatefulSet:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    $cat statefulset.yaml

    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
    name: emqx-statefulset
    labels:
    app: emqx
    spec:
    serviceName: emqx-headless
    updateStrategy:
    type: RollingUpdate
    replicas: 3
    selector:
    matchLabels:
    app: emqx
    template:
    metadata:
    labels:
    app: emqx
    spec:
    serviceAccountName: emqx
    containers:
    - name: emqx
    image: emqx/emqx:v4.1-rc.1
    ports:
    - name: mqtt
    containerPort: 1883
    - name: mqttssl
    containerPort: 8883
    - name: mgmt
    containerPort: 8081
    - name: ws
    containerPort: 8083
    - name: wss
    containerPort: 8084
    - name: dashboard
    containerPort: 18083
    envFrom:
    - configMapRef:
    name: emqx-config

    注意,StatefulSet 需要 Headless Service 来实现稳定的网络标志,因此需要再定义一个 Service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    $cat headless.yaml

    apiVersion: v1
    kind: Service
    metadata:
    name: emqx-headless
    spec:
    type: ClusterIP
    clusterIP: None
    selector:
    app: emqx
    ports:
    - name: mqtt
    port: 1883
    protocol: TCP
    targetPort: 1883
    - name: mqttssl
    port: 8883
    protocol: TCP
    targetPort: 8883
    - name: mgmt
    port: 8081
    protocol: TCP
    targetPort: 8081
    - name: websocket
    port: 8083
    protocol: TCP
    targetPort: 8083
    - name: wss
    port: 8084
    protocol: TCP
    targetPort: 8084
    - name: dashboard
    port: 18083
    protocol: TCP
    targetPort: 18083

    因为 Headless Service 并不需要 IP,所以配置了 clusterIP: None

  • 部署相应的资源:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    $ kubectl apply -f headless-service.yaml
    service/emqx-headless created

    $ kubectl apply -f statefulset.yaml
    statefulset.apps/emqx-deployment created

    $ kubectl get pods
    NAME READY STATUS RESTARTS AGE
    emqx-statefulset-0 1/1 Running 0 2m59s
    emqx-statefulset-1 1/1 Running 0 2m57s
    emqx-statefulset-2 1/1 Running 0 2m54s

    $ kubectl exec emqx-statefulset-0 -- emqx_ctl cluster status
    Cluster status:
    ['emqx@192.168.77.105','emqx@192.168.87.153',
    'emqx@192.168.87.155'],
    stopped_nodes => []}
  • 更新 Configmap:

    StatefulSet 提供了稳定的网络标志,EMQX Broker 支持使用 hostname 和 dns 规则来代提 IP 实现集群,以 hostname 为例,需要修改 emqx.conf

    1
    2
    cluster.kubernetes.address_type = hostname
    cluster.kubernetes.suffix = "svc.cluster.local"

    kubernetes 集群中 Pod 的 DNS 规则可以由用户自定义,EMQX Broker 提供了 cluster.kubernetes.suffix 方便用户匹配自定的 DNS 规则,本文使用默认的 DNS 规则:statefulSetName-{0..N-1}.serviceName.namespace.svc.cluster.local ,DNS 规则中的 serviceName 为 StatefulSet 使用的 Headless Service,所以还需要将 cluster.kubernetes.service_name 修改为 Headless Service Name。

    将配置项转为环境变量,需要在 Configmap 中配置:

    1
    2
    3
    EMQX_CLUSTER__K8S__ADDRESS_TYPE: "hostname"
    EMQX_CLUSTER__K8S__SUFFIX: "svc.cluster.local"
    EMQX_CLUSTER__K8S__SERVICE_NAME: emqx-headless

    Configmap 提供了热更新功能,执行 $ kubectl edit configmap emqx-config 来热更新 Configmap。

  • 重新部署 StatefulSet:

    Configmap 更新之后 Pod 并不会重启,需要我们手动更新 StatefulSet

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    $ kubectl delete statefulset emqx-statefulset
    statefulset.apps "emqx-statefulset" deleted

    $ kubectl apply -f statefulset.yaml
    statefulset.apps/emqx-statefulset created

    $ kubectl get pods
    NAME READY STATUS RESTARTS AGE
    emqx-statefulset-0 1/1 Running 0 115s
    emqx-statefulset-1 1/1 Running 0 112s
    emqx-statefulset-2 1/1 Running 0 110s

    $ kubectl exec emqx-statefulset-2 -- emqx_ctl cluster status
    Cluster status:
    ['emqx@emqx-statefulset-0.emqx-headless.default.svc.cluster.local',
    'emqx@emqx-statefulset-1.emqx-headless.default.svc.cluster.local',
    'emqx@emqx-statefulset-2.emqx-headless.default.svc.cluster.local'],
    stopped_nodes => []}

    可以看到新的 EMQX Broker 集群已经成功的建立起来了。

  • 中止一个 Pod:

    StatefulSet 中的 Pod 重新调度后其 PodName 和 HostName 不变,下面来尝试一下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    $ kubectl get pods
    kuNAME READY STATUS RESTARTS AGE
    emqx-statefulset-0 1/1 Running 0 6m20s
    emqx-statefulset-1 1/1 Running 0 6m17s
    emqx-statefulset-2 1/1 Running 0 6m15s

    $ kubectl delete pod emqx-statefulset-0
    pod "emqx-statefulset-0" deleted

    $ kubectl get pods
    NAME READY STATUS RESTARTS AGE
    emqx-statefulset-0 1/1 Running 0 27s
    emqx-statefulset-1 1/1 Running 0 9m45s
    emqx-statefulset-2 1/1 Running 0 9m43s

    $ kubectl exec emqx-statefulset-2 -- emqx_ctl cluster status
    Cluster status: #{running_nodes =>
    ['emqx@emqx-statefulset-0.emqx-headless.default.svc.cluster.local',
    'emqx@emqx-statefulset-1.emqx-headless.default.svc.cluster.local',
    'emqx@emqx-statefulset-2.emqx-headless.default.svc.cluster.local'],
    stopped_nodes => []}

    跟预期的一样,StatefulSet 重新调度了一个具有相同网络标志的 Pod,Pod 中的 EMQX Broker 也成功的加入了集群。

StorageClasses、PersistentVolume 和 PersistentVolumeClaim

PersistentVolume(PV)是由管理员设置的存储,它是群集的一部分。就像节点是集群中的资源一样,PV 也是集群中的资源。 PV 是 Volume 之类的卷插件,但具有独立于使用 PV 的 Pod 的生命周期。此 API 对象包含存储实现的细节,即 NFS、iSCSI 或特定于云供应商的存储系统。

PersistentVolumeClaim(PVC)是用户存储的请求。它与 Pod 相似。Pod 消耗节点资源,PVC 消耗 PV 资源。Pod 可以请求特定级别的资源(CPU 和内存)。声明可以请求特定的大小和访问模式(例如,可以以读/写一次或 只读多次模式挂载)。

StorageClass 为管理员提供了描述存储 “class(类)” 的方法。 不同的 class 可能会映射到不同的服务质量等级或备份策略,或由群集管理员确定的任意策略。 Kubernetes 本身不清楚各种 class 代表的什么。这个概念在其他存储系统中有时被称为“配置文件”。

在部署 EMQX Broker 的时候,可以预先创建好 PV 或 StorageClass,然后利用 PVC 将 EMQX Broker 的 /opt/emqx/data/mnesia 目录挂载出来,当Pods被重新调度之后,EMQX 会从 /opt/emqx/data/mnesia 目录中获取数据并恢复,从而实现 EMQX Broker 的持久化。

  • 定义 StatefulSet

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    $cat statefulset.yaml

    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
    name: emqx-statefulset
    labels:
    app: emqx
    spec:
    replicas: 3
    serviceName: emqx-headless
    updateStrategy:
    type: RollingUpdate
    selector:
    matchLabels:
    app: emqx
    template:
    metadata:
    labels:
    app: emqx
    spec:
    volumes:
    - name: emqx-data
    persistentVolumeClaim:
    claimName: emqx-pvc
    serviceAccountName: emqx
    containers:
    - name: emqx
    image: emqx/emqx:v4.1-rc.1
    ports:
    - name: mqtt
    containerPort: 1883
    - name: mqttssl
    containerPort: 8883
    - name: mgmt
    containerPort: 8081
    - name: ws
    containerPort: 8083
    - name: wss
    containerPort: 8084
    - name: dashboard
    containerPort: 18083
    envFrom:
    - configMapRef:
    name: emqx-config
    volumeMounts:
    - name: emqx-data
    mountPath: "/opt/emqx/data/mnesia"
    volumeClaimTemplates:
    - metadata:
    name: emqx-pvc
    annotations:
    volume.alpha.kubernetes.io/storage-class: manual
    spec:
    accessModes: [ "ReadWriteOnce" ]
    resources:
    requests:
    storage: 1Gi

    该文件首先通过 volumeClaimTemplates 指定了使用 StorageClass 的 name 为 manual 的存储类创建名称为 emqx-pvc 的 PVC 资源,PVC 资源的读写模式为 ReadWriteOnce,需要 1Gi 的空间,然后将此 PVC 定义为 name 为 emqx-data 的 volumes,并将此 volumes 挂载在 Pod 中的 /opt/emqx/data/mnesia 目录下。

  • 部署资源:

    部署 StatefulSet 之前,需要用户或 kubernetes 集群管理员自行创建存储类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    $ kubectl apply -f statefulset.yaml
    statefulset.apps/emqx-statefulset created

    $ kubectl get pods
    NAME READY STATUS RESTARTS AGE
    emqx-statefulset-0 1/1 Running 0 27s
    emqx-statefulset-1 1/1 Running 0 9m45s
    emqx-statefulset-2 1/1 Running 0 9m43s

    $ kubectl get pvc
    NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
    emqx-data-emqx-statefulset-0 Bound pvc-8094cd75-adb5-11e9-80cc-0697b59e8064 1Gi RWO gp2 2m11s
    emqx-data-emqx-statefulset-0 Bound pvc-9325441d-adb5-11e9-80cc-0697b59e8064 1Gi RWO gp2 99s
    emqx-data-emqx-statefulset-0 Bound pvc-ad425e9d-adb5-11e9-80cc-0697b59e8064 1Gi RWO gp2 56s

    输出结果表明该 PVC 的状态为 Bound,PVC 存储已经成功的建立了,当 Pod 被重新调度时,EMQX Broker 会读取挂载到 PVC 中的数据,从而实现持久化。

EMQX Broker 在 kubernetes 上建立持久化的集群就完成了,本文略过了部分细节,部署的过程也是偏向简单的 Demo,用户可以自行阅读 kubernetes 文档 与 EMQX Team 提供的 Helm chart 源码 来继续深入研究,当然也欢迎在 Github 贡献 issue、pull requests 以及 start。

免费试用 EMQX Cloud

全托管的云原生 MQTT 消息服务

开始试用 →