Այս գրառման մեջ մենք կցանկանայինք կիսվել բաշխված համակարգի կազմաձևման հետ կապված հետաքրքիր ձևով:
Կազմաձևը ներկայացված է անմիջապես Scala լեզվով տիպի անվտանգ ձևով: Իրականացման օրինակը մանրամասն նկարագրված է: Քննարկվում են առաջարկի տարբեր ասպեկտներ, ներառյալ ազդեցությունը ընդհանուր զարգացման գործընթացի վրա:
ներածություն
Ամուր բաշխված համակարգերի կառուցումը պահանջում է ճիշտ և համահունչ կոնֆիգուրացիայի օգտագործում բոլոր հանգույցներում: Տիպիկ լուծում է օգտագործել տեքստային տեղակայման նկարագրությունը (տարածքային, անսխալ կամ նման այլ բան) և ինքնաբերաբար ստեղծվող կազմաձևման ֆայլերը (հաճախ՝ նախատեսված յուրաքանչյուր հանգույցի/դերի համար): Մենք նաև կցանկանայինք օգտագործել նույն տարբերակների նույն արձանագրությունները յուրաքանչյուր հաղորդակցվող հանգույցի վրա (հակառակ դեպքում մենք անհամատեղելիության խնդիրներ կունենայինք): JVM աշխարհում դա նշանակում է, որ առնվազն հաղորդագրությունների գրադարանը պետք է լինի նույն տարբերակով բոլոր հաղորդակցվող հանգույցներում:
Ինչ վերաբերում է համակարգի փորձարկմանը: Իհարկե, մենք պետք է ունենանք միավորի թեստեր բոլոր բաղադրիչների համար, նախքան ինտեգրացիոն թեստերի գալը: Որպեսզի կարողանանք փորձարկման արդյունքներն էքստրապոլյացիա անել գործարկման ժամանակի վրա, մենք պետք է համոզվենք, որ բոլոր գրադարանների տարբերակները նույնական են պահվում ինչպես գործարկման, այնպես էլ փորձարկման միջավայրում:
Ինտեգրման թեստեր իրականացնելիս հաճախ շատ ավելի հեշտ է բոլոր հանգույցներում ունենալ նույն դասընթացը: Մենք պարզապես պետք է համոզվենք, որ նույն դասընթացը օգտագործվում է տեղակայման ժամանակ: (Հնարավոր է օգտագործել տարբեր դասի ուղիներ տարբեր հանգույցների վրա, բայց ավելի դժվար է ներկայացնել այս կոնֆիգուրացիան և ճիշտ տեղակայել այն:) Այսպիսով, ամեն ինչ պարզ պահելու համար մենք միայն կդիտարկենք նույնական դասի ուղիները բոլոր հանգույցներում:
Կազմաձևումը հակված է զարգանալ ծրագրաշարի հետ միասին: Մենք սովորաբար օգտագործում ենք տարբերակներ՝ զանազան բացահայտելու համար
Ծրագրաշարի էվոլյուցիայի փուլերը. Թվում է, թե խելամիտ է ծածկել կոնֆիգուրացիան տարբերակի կառավարման ներքո և որոշել տարբեր կոնֆիգուրացիաներ որոշ պիտակներով: Եթե արտադրության մեջ կա միայն մեկ կոնֆիգուրացիա, մենք կարող ենք օգտագործել մեկ տարբերակը որպես նույնացուցիչ: Երբեմն մենք կարող ենք ունենալ բազմաթիվ արտադրական միջավայրեր: Եվ յուրաքանչյուր միջավայրի համար մեզ կարող է անհրաժեշտ լինել կոնֆիգուրացիայի առանձին ճյուղ: Այսպիսով, կոնֆիգուրացիաները կարող են պիտակավորվել ճյուղով և տարբերակով, որպեսզի եզակի կերպով նույնականացվեն տարբեր կոնֆիգուրացիաները: Յուրաքանչյուր ճյուղի պիտակ և տարբերակ համապատասխանում է յուրաքանչյուր հանգույցի բաշխված հանգույցների, նավահանգիստների, արտաքին ռեսուրսների, դասընթացի գրադարանի տարբերակների մեկ համակցության: Այստեղ մենք կծածկենք միայն մեկ ճյուղը և կբացահայտենք կոնֆիգուրացիան երեք բաղադրիչ տասնորդական տարբերակով (1.2.3), ինչպես մյուս արտեֆակտները:
Ժամանակակից միջավայրերում կազմաձևման ֆայլերը ձեռքով այլևս չեն փոփոխվում: Սովորաբար մենք ստեղծում ենք
կազմաձևման ֆայլերը տեղակայման ժամանակ և
Այս գրառման մեջ մենք կքննարկենք կազմաձևումը կազմված արտեֆակտում պահելու գաղափարը:
Կազմվող կոնֆիգուրացիա
Այս բաժնում մենք կքննարկենք ստատիկ կոնֆիգուրացիայի օրինակ: Կազմաձևվում և ներդրվում են երկու պարզ ծառայություններ՝ echo ծառայությունը և echo ծառայության հաճախորդը: Այնուհետև գործարկվում են երկու տարբեր բաշխված համակարգեր երկու ծառայություններով: Մեկը նախատեսված է մեկ հանգույցի կոնֆիգուրացիայի համար, իսկ մյուսը՝ երկու հանգույցների կազմաձևման համար:
Տիպիկ բաշխված համակարգը բաղկացած է մի քանի հանգույցներից: Հանգույցները կարող են նույնականացվել՝ օգտագործելով որոշ տեսակներ.
sealed trait NodeId
case object Backend extends NodeId
case object Frontend extends NodeId
կամ պարզապես
case class NodeId(hostName: String)
կամ նույնիսկ
object Singleton
type NodeId = Singleton.type
Այս հանգույցները կատարում են տարբեր դերեր, գործարկում են որոշ ծառայություններ և պետք է կարողանան շփվել մյուս հանգույցների հետ TCP/HTTP կապերի միջոցով:
TCP կապի համար պահանջվում է առնվազն պորտի համար: Մենք նաև ցանկանում ենք համոզվել, որ հաճախորդը և սերվերը խոսում են նույն արձանագրության մասին: Հանգույցների միջև կապը մոդելավորելու համար հայտարարենք հետևյալ դասը.
case class TcpEndPoint[Protocol](node: NodeId, port: Port[Protocol])
որտեղ Port
ընդամենը մի է Int
թույլատրելի միջակայքում.
type PortNumber = Refined[Int, Closed[_0, W.`65535`.T]]
Զտված տեսակներ
Տեսնել Int
թույլատրվում է ունենալ միայն 16-բիթանոց արժեքներ, որոնք կարող են ներկայացնել պորտի համարը: Այս կոնֆիգուրացիայի մոտեցման համար այս գրադարանն օգտագործելու պահանջ չկա: Պարզապես կարծես թե շատ լավ է տեղավորվում:
HTTP-ի (REST) համար մեզ կարող է անհրաժեշտ լինել նաև ծառայության ուղի.
type UrlPathPrefix = Refined[String, MatchesRegex[W.`"[a-zA-Z_0-9/]*"`.T]]
case class PortWithPrefix[Protocol](portNumber: PortNumber, pathPrefix: UrlPathPrefix)
Ֆանտոմի տեսակ
Կազմման ընթացքում արձանագրությունը բացահայտելու համար մենք օգտագործում ենք Scala հատկանիշը, որը հայտարարում է տիպի փաստարկը Protocol
որը չի օգտագործվում դասարանում։ Դա այսպես կոչված ուրվական տեսակ. Գործարկման ժամանակ մեզ հազվադեպ է անհրաժեշտ արձանագրության նույնացուցիչի օրինակ, այդ իսկ պատճառով մենք այն չենք պահում: Կազմման ընթացքում այս ֆանտոմային տեսակը տալիս է լրացուցիչ տիպի անվտանգություն։ Մենք չենք կարող փոխանցել նավահանգիստը սխալ արձանագրությամբ:
Ամենալայն օգտագործվող արձանագրություններից մեկը REST API-ն է Json սերիալիզացիայով.
sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]
որտեղ RequestMessage
հաղորդագրությունների հիմնական տեսակն է, որը հաճախորդը կարող է ուղարկել սերվեր և ResponseMessage
պատասխան հաղորդագրություն է սերվերից: Իհարկե, մենք կարող ենք ստեղծել այլ արձանագրությունների նկարագրություններ, որոնք կսահմանեն հաղորդակցման արձանագրությունը ցանկալի ճշգրտությամբ:
Այս գրառման նպատակների համար մենք կօգտագործենք արձանագրության ավելի պարզ տարբերակը.
sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]
Այս արձանագրության մեջ հարցման հաղորդագրությունը կցվում է url-ին և պատասխան հաղորդագրությունը վերադարձվում է որպես պարզ տող:
Ծառայության կոնֆիգուրացիան կարող է նկարագրվել ծառայության անունով, նավահանգիստների հավաքածուով և որոշ կախվածություններով: Կան մի քանի հնարավոր եղանակներ, թե ինչպես ներկայացնել այս բոլոր տարրերը Scala-ում (օրինակ. HList
, հանրահաշվական տվյալների տեսակները): Այս գրառման նպատակների համար մենք կօգտագործենք Cake Pattern-ը և կներկայացնենք համատեղելի կտորներ (մոդուլներ) որպես հատկություններ: (Cake Pattern-ը պարտադիր չէ այս կազմաձևման մոտեցման համար: Դա գաղափարի հնարավոր իրականացումն է միայն:)
Կախվածությունները կարող են ներկայացվել՝ օգտագործելով Cake Pattern-ը որպես այլ հանգույցների վերջնակետեր.
type EchoProtocol[A] = SimpleHttpGetRest[A, A]
trait EchoConfig[A] extends ServiceConfig {
def portNumber: PortNumber = 8081
def echoPort: PortWithPrefix[EchoProtocol[A]] = PortWithPrefix[EchoProtocol[A]](portNumber, "echo")
def echoService: HttpSimpleGetEndPoint[NodeId, EchoProtocol[A]] = providedSimpleService(echoPort)
}
Echo ծառայությանը միայն անհրաժեշտ է կարգավորել նավահանգիստը: Եվ մենք հայտարարում ենք, որ այս նավահանգիստն աջակցում է echo արձանագրությանը: Նկատի ունեցեք, որ այս պահին մենք կարիք չունենք որոշակի պորտ նշելու, քանի որ հատկանիշը թույլ է տալիս վերացական մեթոդների հայտարարություններ: Եթե մենք օգտագործում ենք վերացական մեթոդներ, կոմպիլյատորը կպահանջի իրականացում կոնֆիգուրացիայի օրինակում: Այստեղ մենք տրամադրել ենք իրականացումը (8081
) և այն կօգտագործվի որպես լռելյայն արժեք, եթե այն բաց թողնենք կոնկրետ կոնֆիգուրացիայի մեջ:
Մենք կարող ենք հայտարարել կախվածություն echo ծառայության հաճախորդի կազմաձևում.
trait EchoClientConfig[A] {
def testMessage: String = "test"
def pollInterval: FiniteDuration
def echoServiceDependency: HttpSimpleGetEndPoint[_, EchoProtocol[A]]
}
Կախվածությունն ունի նույն տեսակը, ինչ որ echoService
. Մասնավորապես, պահանջում է նույն արձանագրությունը։ Այսպիսով, մենք կարող ենք վստահ լինել, որ եթե կապենք այս երկու կախվածությունները, դրանք ճիշտ կաշխատեն:
Ծառայությունների իրականացում
Ծառայությանը անհրաժեշտ է գործառույթ՝ գործարկելու և նրբորեն անջատելու համար: (Ծառայությունն անջատելու ունակությունը կարևոր է թեստավորման համար:) Կրկին կան մի քանի տարբերակներ տվյալ կոնֆիգուրացիայի համար նման գործառույթ նշելու համար (օրինակ, մենք կարող ենք օգտագործել տիպի դասեր): Այս գրառման համար մենք կրկին կօգտագործենք Cake Pattern-ը: Մենք կարող ենք ներկայացնել ծառայություն՝ օգտագործելով cats.Resource
որն արդեն ապահովում է փակագծում և ռեսուրսների թողարկում: Ռեսուրս ձեռք բերելու համար մենք պետք է տրամադրենք կոնֆիգուրացիա և որոշակի գործարկման համատեքստ: Այսպիսով, ծառայության մեկնարկի գործառույթը կարող է նման լինել.
type ResourceReader[F[_], Config, A] = Reader[Config, Resource[F, A]]
trait ServiceImpl[F[_]] {
type Config
def resource(
implicit
resolver: AddressResolver[F],
timer: Timer[F],
contextShift: ContextShift[F],
ec: ExecutionContext,
applicative: Applicative[F]
): ResourceReader[F, Config, Unit]
}
որտեղ
Config
— կոնֆիգուրացիայի տեսակը, որը պահանջվում է ծառայության այս մեկնարկչի կողմիցAddressResolver
— գործարկման ժամանակի օբյեկտ, որն ունի այլ հանգույցների իրական հասցեներ ստանալու հնարավորություն (մանրամասների համար շարունակեք կարդալ):
մյուս տեսակները գալիս են cats
:
F[_]
— էֆեկտի տեսակը (ամենապարզ դեպքումF[A]
կարող է լինել պարզապես() => A
. Այս գրառման մեջ մենք կօգտագործենքcats.IO
.)Reader[A,B]
— քիչ թե շատ ֆունկցիայի հոմանիշ էA => B
cats.Resource
— ունի ձեռք բերելու և ազատելու ուղիներTimer
— թույլ է տալիս քնել/չափել ժամանակըContextShift
- անալոգայինExecutionContext
Applicative
— գործող ֆունկցիաների փաթաթում (գրեթե մոնադ) (մենք կարող ենք, ի վերջո, այն փոխարինել այլ բանով)
Օգտագործելով այս ինտերֆեյսը, մենք կարող ենք իրականացնել մի քանի ծառայություններ: Օրինակ, ծառայություն, որը ոչինչ չի անում.
trait ZeroServiceImpl[F[_]] extends ServiceImpl[F] {
type Config <: Any
def resource(...): ResourceReader[F, Config, Unit] =
Reader(_ => Resource.pure[F, Unit](()))
}
(Տես
Հանգույցը մեկ օբյեկտ է, որն աշխատում է մի քանի ծառայություններով (ռեսուրսների շղթայի մեկնարկը միացված է Cake Pattern-ի կողմից).
object SingleNodeImpl extends ZeroServiceImpl[IO]
with EchoServiceService
with EchoClientService
with FiniteDurationLifecycleServiceImpl
{
type Config = EchoConfig[String] with EchoClientConfig[String] with FiniteDurationLifecycleConfig
}
Նկատի ունեցեք, որ հանգույցում մենք նշում ենք կոնֆիգուրացիայի ճշգրիտ տեսակը, որն անհրաժեշտ է այս հանգույցին: Կոմպիլյատորը թույլ չի տա մեզ կառուցել օբյեկտը (Cake) անբավարար տիպով, քանի որ ծառայության յուրաքանչյուր հատկանիշ հայտարարում է սահմանափակումներ Config
տիպ. Նաև մենք չենք կարողանա սկսել հանգույցը առանց ամբողջական կազմաձևման:
Հանգույցի հասցեի լուծում
Կապ հաստատելու համար մեզ անհրաժեշտ է իրական հյուրընկալող հասցե յուրաքանչյուր հանգույցի համար: Դա կարող է հայտնի լինել ավելի ուշ, քան կազմաձևման այլ մասերը: Հետևաբար, մեզ անհրաժեշտ է հանգույցի id-ի և դրա իրական հասցեի միջև քարտեզագրում տրամադրելու միջոց: Այս քարտեզագրումը գործառույթ է.
case class NodeAddress[NodeId](host: Uri.Host)
trait AddressResolver[F[_]] {
def resolve[NodeId](nodeId: NodeId): F[NodeAddress[NodeId]]
}
Նման գործառույթն իրականացնելու մի քանի հնարավոր եղանակներ կան:
- Եթե մենք գիտենք իրական հասցեները նախքան տեղակայումը, հանգույցների հոսթինգի ինստանցիոնացման ժամանակ, ապա մենք կարող ենք գեներացնել Scala կոդը իրական հասցեներով և այնուհետև գործարկել build-ը (որը կատարում է կոմպիլյացիայի ժամանակի ստուգումներ և այնուհետև գործարկում է ինտեգրման թեստային փաթեթը): Այս դեպքում մեր քարտեզագրման ֆունկցիան ստատիկորեն հայտնի է և կարող է պարզեցվել a-ի նման
Map[NodeId, NodeAddress]
. - Երբեմն մենք փաստացի հասցեներ ենք ստանում միայն ավելի ուշ, երբ հանգույցն իրականում սկսվում է, կամ մենք չունենք հանգույցների հասցեներ, որոնք դեռ չեն սկսվել: Այս դեպքում մենք կարող ենք ունենալ հայտնաբերման ծառայություն, որը մեկնարկել է բոլոր մյուս հանգույցներից առաջ, և յուրաքանչյուր հանգույց կարող է գովազդել իր հասցեն այդ ծառայության մեջ և բաժանորդագրվել կախվածություններին:
- Եթե մենք կարողանանք փոփոխել
/etc/hosts
, մենք կարող ենք օգտագործել նախապես սահմանված հյուրընկալող անունները (ինչպեսmy-project-main-node
ևecho-backend
) և տեղադրման պահին ուղղակի կապեք այս անունը ip հասցեի հետ:
Այս գրառման մեջ մենք ավելի մանրամասն չենք անդրադառնում այս դեպքերին: Փաստորեն, մեր խաղալիքի օրինակում բոլոր հանգույցները կունենան նույն IP հասցեն. 127.0.0.1
.
Այս գրառման մեջ մենք կքննարկենք երկու բաշխված համակարգի դասավորությունը.
- Մեկ հանգույցի դասավորություն, որտեղ բոլոր ծառայությունները տեղադրվում են մեկ հանգույցի վրա:
- Երկու հանգույցի դասավորություն, որտեղ ծառայությունը և հաճախորդը գտնվում են տարբեր հանգույցների վրա:
Կոնֆիգուրացիան ա
Մեկ հանգույցի կոնֆիգուրացիա
object SingleNodeConfig extends EchoConfig[String]
with EchoClientConfig[String] with FiniteDurationLifecycleConfig
{
case object Singleton // identifier of the single node
// configuration of server
type NodeId = Singleton.type
def nodeId = Singleton
/** Type safe service port specification. */
override def portNumber: PortNumber = 8088
// configuration of client
/** We'll use the service provided by the same host. */
def echoServiceDependency = echoService
override def testMessage: UrlPathElement = "hello"
def pollInterval: FiniteDuration = 1.second
// lifecycle controller configuration
def lifetime: FiniteDuration = 10500.milliseconds // additional 0.5 seconds so that there are 10 requests, not 9.
}
Այստեղ մենք ստեղծում ենք մեկ կոնֆիգուրացիա, որը ընդլայնում է ինչպես սերվերի, այնպես էլ հաճախորդի կազմաձևումը: Նաև մենք կարգավորում ենք կյանքի ցիկլի վերահսկիչ, որը սովորաբար կդադարեցնի հաճախորդը և սերվերը դրանից հետո lifetime
ինտերվալային անցնում.
Ծառայությունների իրականացման և կոնֆիգուրացիաների նույն փաթեթը կարող է օգտագործվել երկու առանձին հանգույցներով համակարգի դասավորությունը ստեղծելու համար: Մեզ պարզապես պետք է ստեղծագործել
Երկու հանգույցների կոնֆիգուրացիա
object NodeServerConfig extends EchoConfig[String] with SigTermLifecycleConfig
{
type NodeId = NodeIdImpl
def nodeId = NodeServer
override def portNumber: PortNumber = 8080
}
object NodeClientConfig extends EchoClientConfig[String] with FiniteDurationLifecycleConfig
{
// NB! dependency specification
def echoServiceDependency = NodeServerConfig.echoService
def pollInterval: FiniteDuration = 1.second
def lifetime: FiniteDuration = 10500.milliseconds // additional 0.5 seconds so that there are 10 request, not 9.
def testMessage: String = "dolly"
}
Տեսեք, թե ինչպես ենք մենք նշում կախվածությունը: Մենք նշում ենք մյուս հանգույցի մատուցած ծառայությունը որպես ընթացիկ հանգույցի կախվածություն: Կախվածության տեսակը ստուգված է, քանի որ այն պարունակում է ֆանտոմային տիպ, որը նկարագրում է արձանագրությունը: Եվ գործարկման ժամանակ մենք կունենանք ճիշտ հանգույցի ID-ն: Սա առաջարկվող կոնֆիգուրացիայի մոտեցման կարևոր կողմերից մեկն է: Այն մեզ հնարավորություն է տալիս միայն մեկ անգամ տեղադրել նավահանգիստը և համոզվել, որ մենք հղում ենք կատարում ճիշտ պորտին:
Երկու հանգույցների իրականացում
Այս կոնֆիգուրացիայի համար մենք օգտագործում ենք նույն ծառայությունների իրականացումները: Ընդհանրապես ոչ մի փոփոխություն: Այնուամենայնիվ, մենք ստեղծում ենք երկու տարբեր հանգույցների իրականացում, որոնք պարունակում են տարբեր ծառայություններ.
object TwoJvmNodeServerImpl extends ZeroServiceImpl[IO] with EchoServiceService with SigIntLifecycleServiceImpl {
type Config = EchoConfig[String] with SigTermLifecycleConfig
}
object TwoJvmNodeClientImpl extends ZeroServiceImpl[IO] with EchoClientService with FiniteDurationLifecycleServiceImpl {
type Config = EchoClientConfig[String] with FiniteDurationLifecycleConfig
}
Առաջին հանգույցը իրականացնում է սերվեր, և դրա համար անհրաժեշտ է միայն սերվերի կողմի կոնֆիգուրացիա: Երկրորդ հանգույցը իրականացնում է հաճախորդը և կարիք ունի կազմաձևման մեկ այլ մասի: Երկու հանգույցներն էլ պահանջում են որոշակի ժամկետի հստակեցում: Այս փոստային ծառայության նպատակների համար հանգույցը կունենա անսահման կյանք, որը կարող է դադարեցվել օգտագործելով SIGTERM
, մինչդեռ echo հաճախորդը կդադարեցվի կազմաձևված վերջավոր տևողությունից հետո: Տեսեք
Ընդհանուր զարգացման գործընթաց
Տեսնենք, թե ինչպես է այս մոտեցումը փոխում կոնֆիգուրացիայի հետ աշխատելու ձևը:
Կոդի կազմաձևը կկազմվի և կստեղծի արտեֆակտ: Խելամիտ է թվում կոնֆիգուրացիայի արտեֆակտը առանձնացնել կոդերի այլ արտեֆակտներից: Հաճախ մենք կարող ենք ունենալ բազմաթիվ կոնֆիգուրացիաներ նույն կոդի բազայի վրա: Եվ իհարկե, մենք կարող ենք ունենալ տարբեր կոնֆիգուրացիայի ճյուղերի մի քանի տարբերակներ: Կազմաձևում մենք կարող ենք ընտրել գրադարանների որոշակի տարբերակներ, և դա անփոփոխ կմնա ամեն անգամ, երբ մենք տեղադրենք այս կոնֆիգուրացիան:
Կազմաձևի փոփոխությունը դառնում է կոդի փոփոխություն: Այսպիսով, այն պետք է ծածկվի որակի ապահովման նույն գործընթացով.
Տոմս -> PR -> վերանայում -> միաձուլում -> շարունակական ինտեգրում -> շարունակական տեղակայում
Մոտեցման հետևյալ հետևանքները կան.
- Կազմաձևը համահունչ է որոշակի համակարգի օրինակի համար: Թվում է, թե հանգույցների միջև սխալ կապ ունենալու հնարավորություն չկա։
- Հեշտ չէ փոխել կոնֆիգուրացիան միայն մեկ հանգույցում: Թվում է, թե անհիմն է մուտք գործել և փոխել որոշ տեքստային ֆայլեր: Այսպիսով, կոնֆիգուրացիայի շեղումը դառնում է ավելի քիչ հնարավոր:
- Փոքր կոնֆիգուրացիայի փոփոխությունները հեշտ չէ կատարել:
- Կազմաձևման փոփոխությունների մեծ մասը կհետևի նույն զարգացման գործընթացին, և այն կանցնի որոշակի վերանայում:
Արդյո՞ք մեզ անհրաժեշտ է առանձին պահեստ՝ արտադրության կազմաձևման համար: Արտադրության կոնֆիգուրացիան կարող է պարունակել զգայուն տեղեկատվություն, որը մենք կցանկանայինք հեռու պահել շատ մարդկանցից: Այսպիսով, գուցե արժե առանձին պահոց պահել սահմանափակ մուտքով, որը կպարունակի արտադրության կոնֆիգուրացիան: Մենք կարող ենք կոնֆիգուրացիան բաժանել երկու մասի՝ մեկը, որը պարունակում է արտադրության առավել բաց պարամետրերը, և մեկը, որը պարունակում է կոնֆիգուրացիայի գաղտնի մասը: Սա թույլ կտա մշակողների մեծամասնությանը մուտք գործել պարամետրերի ճնշող մեծամասնություն՝ միաժամանակ սահմանափակելով մուտքը իսկապես զգայուն բաների: Դա հեշտ է իրականացնել դա՝ օգտագործելով միջանկյալ հատկանիշները լռելյայն պարամետրերի արժեքներով:
Տարբերակներ
Տեսնենք առաջարկվող մոտեցման դրական և բացասական կողմերը՝ համեմատած այլ կոնֆիգուրացիայի կառավարման տեխնիկայի հետ:
Նախ, մենք կթվարկենք մի քանի այլընտրանքներ կոնֆիգուրացիայի հետ կապված առաջարկվող եղանակի տարբեր ասպեկտների համար.
- Տեքստային ֆայլ թիրախային մեքենայի վրա:
- Կենտրոնացված բանալի-արժեքի պահեստավորում (օրինակ
etcd
/zookeeper
). - Ենթագործընթացի բաղադրիչներ, որոնք կարող են վերակազմավորվել/վերագործարկվել առանց վերագործարկման գործընթացի:
- Արտեֆակտից դուրս կոնֆիգուրացիա և տարբերակի վերահսկում:
Տեքստային ֆայլը որոշակի ճկունություն է տալիս ժամանակավոր շտկումների առումով: Համակարգի ադմինիստրատորը կարող է մուտք գործել թիրախային հանգույց, կատարել փոփոխություն և պարզապես վերագործարկել ծառայությունը: Սա կարող է այնքան էլ լավ չլինել ավելի մեծ համակարգերի համար: Փոփոխության հետևում հետքեր չեն մնացել։ Փոփոխությունը չի վերանայվում մեկ այլ զույգ աչքով: Հնարավոր է, որ դժվար լինի պարզել, թե ինչն է փոխել փոփոխությունը: Այն չի փորձարկվել։ Բաշխված համակարգի տեսանկյունից ադմինիստրատորը կարող է պարզապես մոռանալ թարմացնել կոնֆիգուրացիան մյուս հանգույցներից մեկում:
(Btw, եթե ի վերջո անհրաժեշտություն առաջանա սկսել օգտագործել տեքստային կազմաձևման ֆայլերը, մենք միայն պետք է ավելացնենք վերլուծիչ + վավերացուցիչ, որը կարող է արտադրել նույնը Config
մուտքագրեք և դա բավական կլինի տեքստային կոնֆիգուրացիաներ օգտագործելու համար: Սա նաև ցույց է տալիս, որ կոմպիլյացիայի ժամանակի կազմաձևման բարդությունը մի փոքր ավելի փոքր է, քան տեքստի վրա հիմնված կազմաձևերի բարդությունը, քանի որ տեքստի վրա հիմնված տարբերակում մեզ անհրաժեշտ է լրացուցիչ կոդ):
Կենտրոնացված բանալի-արժեքի պահեստավորումը լավ մեխանիզմ է հավելվածի մետա պարամետրերը բաշխելու համար: Այստեղ մենք պետք է մտածենք այն մասին, թե որոնք ենք մենք համարում կազմաձևման արժեքներ և ինչ են պարզապես տվյալներ: Տրվում է ֆունկցիա C => A => B
մենք սովորաբար անվանում ենք հազվադեպ փոփոխվող արժեքներ C
«կոնֆիգուրացիա», մինչդեռ հաճախակի փոփոխվող տվյալները A
- պարզապես մուտքագրեք տվյալները: Կազմաձևումը պետք է տրամադրվի գործառույթին ավելի վաղ, քան տվյալները A
. Հաշվի առնելով այս գաղափարը, մենք կարող ենք ասել, որ փոփոխությունների ակնկալվող հաճախականությունը կարող է օգտագործվել միայն տվյալներից տարբերելու կազմաձևման տվյալները: Նաև տվյալները սովորաբար գալիս են մեկ աղբյուրից (օգտագործողից), իսկ կոնֆիգուրացիան գալիս է մեկ այլ աղբյուրից (ադմինիստրատոր): Պարամետրերի հետ գործ ունենալը, որոնք կարող են փոխվել սկզբնավորման գործընթացից հետո, հանգեցնում է հավելվածի բարդության մեծացման: Նման պարամետրերի համար մենք պետք է կարգավորենք դրանց առաքման մեխանիզմը, վերլուծությունը և վավերացումը, սխալ արժեքների մշակումը: Հետևաբար, ծրագրի բարդությունը նվազեցնելու համար ավելի լավ է նվազեցնել այն պարամետրերի քանակը, որոնք կարող են փոփոխվել գործարկման ժամանակ (կամ նույնիսկ ընդհանրապես վերացնել դրանք):
Այս գրառման տեսանկյունից մենք պետք է տարբերակենք ստատիկ և դինամիկ պարամետրերը: Եթե ծառայության տրամաբանությունը պահանջում է որոշ պարամետրերի հազվադեպ փոփոխություն գործարկման ժամանակ, ապա մենք կարող ենք դրանք անվանել դինամիկ պարամետրեր: Հակառակ դեպքում դրանք ստատիկ են և կարող են կազմաձևվել՝ օգտագործելով առաջարկվող մոտեցումը: Դինամիկ վերակազմավորման համար կարող են անհրաժեշտ լինել այլ մոտեցումներ: Օրինակ, համակարգի մասերը կարող են վերագործարկվել նոր կազմաձևման պարամետրերով, ինչպես բաշխված համակարգի առանձին գործընթացների վերագործարկումը:
(Իմ համեստ կարծիքն է՝ խուսափել գործարկման ժամանակի վերակազմավորումից, քանի որ դա մեծացնում է համակարգի բարդությունը:
Ավելի պարզ կարող է լինել պարզապես ապավինել OS-ի աջակցությանը վերագործարկման գործընթացներում: Այնուամենայնիվ, դա միշտ չէ, որ հնարավոր է:)
Ստատիկ կոնֆիգուրացիայի օգտագործման կարևոր ասպեկտներից մեկը, որը երբեմն ստիպում է մարդկանց դիտարկել դինամիկ կազմաձևումը (առանց այլ պատճառների), ծառայության խափանումն է կազմաձևման թարմացման ժամանակ: Իրոք, եթե մենք պետք է փոփոխություններ կատարենք ստատիկ կազմաձևում, մենք պետք է վերագործարկենք համակարգը, որպեսզի նոր արժեքները դառնան արդյունավետ: Դադարեցման պահանջները տարբեր համակարգերի համար տարբեր են, ուստի այն կարող է այդքան էլ կարևոր չլինել: Եթե դա կարևոր է, ապա մենք պետք է նախապես պլանավորենք ցանկացած համակարգի վերագործարկում: Օրինակ, մենք կարող էինք իրականացնել
Ինչ վերաբերում է կոնֆիգուրացիայի պահպանմանը տարբերակված արտեֆակտի ներսում կամ դրսում: Արտեֆակտի ներսում կոնֆիգուրացիան պահելը նշանակում է, որ շատ դեպքերում այս կոնֆիգուրացիան անցել է որակի ապահովման նույն գործընթացը, ինչ մյուս արտեֆակտները: Այսպիսով, կարելի է վստահ լինել, որ կոնֆիգուրացիան լավ որակի է և վստահելի: Ընդհակառակը, առանձին ֆայլում կոնֆիգուրացիան նշանակում է, որ հետքեր չկան, թե ով և ինչու է փոփոխություններ կատարել այդ ֆայլում: Արդյո՞ք սա կարևոր է: Մենք կարծում ենք, որ արտադրական համակարգերի մեծ մասի համար ավելի լավ է ունենալ կայուն և բարձրորակ կոնֆիգուրացիա:
Արտեֆակտի տարբերակը թույլ է տալիս պարզել, թե երբ է այն ստեղծվել, ինչ արժեքներ է պարունակում, ինչ հնարավորություններ են միացված/անջատված, ով է պատասխանատու կոնֆիգուրացիայի յուրաքանչյուր փոփոխություն կատարելու համար: Դա կարող է որոշակի ջանք պահանջել արտեֆակտի ներսում կոնֆիգուրացիան պահպանելու համար, և դա դիզայնի ընտրություն է:
Կողմ և դեմ
Այստեղ մենք կցանկանայինք առանձնացնել որոշ առավելություններ և քննարկել առաջարկվող մոտեցման որոշ թերություններ:
Առավելությունները
Ամբողջական բաշխված համակարգի կոմպիլյացիաների կազմաձևման առանձնահատկությունները.
- Կազմաձևի ստատիկ ստուգում: Սա վստահության բարձր մակարդակ է տալիս, որ կոնֆիգուրացիան ճիշտ է՝ հաշվի առնելով տեսակի սահմանափակումները:
- Կազմաձևման հարուստ լեզու: Սովորաբար այլ կազմաձևման մոտեցումները սահմանափակվում են առավելագույնը փոփոխական փոխարինմամբ:
Օգտագործելով Scala-ն, կարելի է օգտագործել լեզվական առանձնահատկությունների լայն շրջանակ՝ կազմաձևումն ավելի լավը դարձնելու համար: Օրինակ, մենք կարող ենք օգտագործել հատկանիշներ՝ լռելյայն արժեքներ տրամադրելու համար, օբյեկտներ՝ տարբեր շրջանակներ սահմանելու համար, կարող ենք անդրադառնալval
s սահմանվում է միայն մեկ անգամ արտաքին շրջանակում (DRY): Հնարավոր է օգտագործել բառացի հաջորդականություններ կամ որոշակի դասերի օրինակներ (Seq
,Map
Եւ այլն): - DSL. Scala-ն պատշաճ աջակցություն ունի DSL գրողների համար: Կարելի է օգտագործել այս հնարավորությունները՝ ստեղծելու կազմաձևման լեզու, որն ավելի հարմար և վերջնական օգտագործողի համար հարմար է, որպեսզի վերջնական կազմաձևը գոնե ընթեռնելի լինի տիրույթի օգտագործողների համար:
- Ամբողջականություն և համախմբվածություն հանգույցների միջև: Ամբողջ բաշխված համակարգի մեկ վայրում կազմաձևման առավելություններից մեկն այն է, որ բոլոր արժեքները սահմանվում են խստորեն մեկ անգամ և այնուհետև նորից օգտագործվում են բոլոր այն վայրերում, որտեղ դրանք մեզ անհրաժեշտ են: Նաև մուտքագրեք անվտանգ նավահանգիստների հայտարարագրերը, որպեսզի ապահովեն, որ բոլոր հնարավոր ճիշտ կոնֆիգուրացիաներում համակարգի հանգույցները կխոսեն նույն լեզվով: Հանգույցների միջև կան բացահայտ կախվածություններ, ինչը դժվարացնում է որոշ ծառայություններ մատուցելու մասին մոռանալը:
- Փոփոխությունների բարձր որակ: Կազմաձևի փոխանցման ընդհանուր մոտեցումը փոխվում է նորմալ PR գործընթացի միջոցով, սահմանում է որակի բարձր չափանիշներ նաև կոնֆիգուրացիայի մեջ:
- Միաժամանակյա կոնֆիգուրացիայի փոփոխություններ: Ամեն անգամ, երբ մենք որևէ փոփոխություն ենք կատարում կազմաձևման մեջ, ավտոմատ տեղակայումը ապահովում է, որ բոլոր հանգույցները թարմացվում են:
- Դիմումի պարզեցում. Հավելվածը կարիք չունի վերլուծելու և վավերացնելու կոնֆիգուրացիան և կարգավորելու սխալ կազմաձևման արժեքները: Սա հեշտացնում է ընդհանուր կիրառումը: (Բարդության որոշակի աճը հենց կազմաձևման մեջ է, բայց դա գիտակցված փոխզիջում է անվտանգության նկատմամբ:) Սովորական կազմաձևին վերադառնալը բավականին պարզ է. պարզապես ավելացրեք բացակայող մասերը: Ավելի հեշտ է սկսել կոմպիլյացիան և հետաձգել լրացուցիչ մասերի իրականացումը ավելի ուշ:
- Տարբերակված կոնֆիգուրացիա: Շնորհիվ այն բանի, որ կոնֆիգուրացիայի փոփոխությունները հետևում են զարգացման նույն գործընթացին, արդյունքում մենք ստանում ենք եզակի տարբերակով արտեֆակտ: Այն մեզ թույլ է տալիս անհրաժեշտության դեպքում հետ փոխարկել կազմաձևը: Մենք նույնիսկ կարող ենք տեղակայել մեկ տարի առաջ օգտագործված կոնֆիգուրացիան, և այն կաշխատի ճիշտ նույն կերպ: Կայուն կոնֆիգուրացիան բարելավում է բաշխված համակարգի կանխատեսելիությունը և հուսալիությունը: Կազմաձևը ամրագրված է կոմպիլյացիայի ժամանակ և չի կարող հեշտությամբ խախտվել արտադրական համակարգում:
- Մոդուլյարություն. Առաջարկվող շրջանակը մոդուլային է, և մոդուլները կարող են համակցվել տարբեր ձևերով
աջակցում է տարբեր կոնֆիգուրացիաների (կարգավորումներ/դասավորություններ): Մասնավորապես, հնարավոր է ունենալ փոքր մասշտաբի մեկ հանգույցի դասավորություն և լայնածավալ բազմահանգույցի կարգավորում: Խելամիտ է ունենալ արտադրության բազմաթիվ դասավորություններ: - Փորձարկում. Փորձարկման նպատակով կարելի է կիրառել կեղծ ծառայություն և օգտագործել այն որպես կախվածություն տիպային անվտանգ եղանակով: Մի քանի տարբեր փորձարկման դասավորություններ՝ տարբեր մասերով, որոնք փոխարինված են ծաղրերով, կարող են միաժամանակ պահպանվել:
- Ինտեգրման փորձարկում. Երբեմն բաշխված համակարգերում դժվար է իրականացնել ինտեգրման թեստեր: Օգտագործելով նկարագրված մոտեցումը՝ ամբողջական բաշխված համակարգի անվտանգ կոնֆիգուրացիան մուտքագրելու համար, մենք կարող ենք կառավարելի եղանակով գործարկել բոլոր բաշխված մասերը մեկ սերվերի վրա: Հեշտ է նմանակել իրավիճակը
երբ ծառայություններից մեկը դառնում է անհասանելի:
Թերությունները
Կազմված կազմաձևման մոտեցումը տարբերվում է «նորմալ» կոնֆիգուրացիայից և կարող է չհամապատասխանել բոլոր կարիքներին: Ահա կազմված կազմաձևի թերություններից մի քանիսը.
- Ստատիկ կոնֆիգուրացիա: Հնարավոր է, որ այն հարմար չէ բոլոր ծրագրերի համար: Որոշ դեպքերում անհրաժեշտ է արագ ֆիքսել կոնֆիգուրացիան արտադրության մեջ՝ շրջանցելով անվտանգության բոլոր միջոցները: Այս մոտեցումն ավելի է դժվարացնում: Կազմումը և վերաբաշխումը պահանջվում են կազմաձևման ցանկացած փոփոխություն կատարելուց հետո: Սա և՛ հատկանիշն է, և՛ բեռը։
- Կազմաձևման ձևավորում: Երբ կազմաձևումը ստեղծվում է ավտոմատացման որևէ գործիքի կողմից, այս մոտեցումը պահանջում է հետագա հավաքում (որն իր հերթին կարող է ձախողվել): Այս լրացուցիչ քայլը կառուցման համակարգում ինտեգրելու համար կարող է լրացուցիչ ջանքեր պահանջվել:
- Գործիքներ. Այսօր օգտագործվում են բազմաթիվ գործիքներ, որոնք հիմնված են տեքստի վրա հիմնված կազմաձևերի վրա: Նրանցից ոմանք
կիրառելի չի լինի, երբ կազմաձևումը կազմվի: - Մտածողության փոփոխություն է անհրաժեշտ։ Մշակողները և DevOps-ը ծանոթ են տեքստային կազմաձևման ֆայլերին: Կոնֆիգուրացիա կազմելու գաղափարը կարող է տարօրինակ թվալ նրանց:
- Նախքան կոմպիլյատոր կոնֆիգուրացիան ներդնելը պահանջվում է բարձրորակ ծրագրային ապահովման մշակման գործընթաց:
Իրականացված օրինակի որոշ սահմանափակումներ կան.
- Եթե մենք տրամադրենք լրացուցիչ կոնֆիգուր, որը չի պահանջվում հանգույցի իրականացման համար, կոմպիլյատորը չի օգնի մեզ բացահայտել բացակայող իրականացումը: Սա կարելի է լուծել՝ օգտագործելով
HList
կամ ADTs (case classes) հանգույցների կազմաձևման համար՝ հատկանիշների և Cake Pattern-ի փոխարեն: - Մենք պետք է տրամադրենք մի քանի կաթսա կոնֆիգուրացիայի ֆայլում.
package
,import
,object
հայտարարություններ;
override def
-ը այն պարամետրերի համար, որոնք ունեն լռելյայն արժեքներ): Սա կարող է մասամբ լուծվել DSL-ի միջոցով: - Այս գրառման մեջ մենք չենք լուսաբանում նմանատիպ հանգույցների կլաստերների դինամիկ վերակազմավորումը:
Եզրափակում
Այս գրառման մեջ մենք քննարկել ենք կոնֆիգուրացիան ուղղակիորեն սկզբնաղբյուրում տիպային անվտանգ ձևով ներկայացնելու գաղափարը: Մոտեցումը կարող է օգտագործվել շատ ծրագրերում՝ որպես xml-ի և տեքստի վրա հիմնված այլ կազմաձևերի փոխարինում: Չնայած այն հանգամանքին, որ մեր օրինակը ներդրվել է Scala-ում, այն կարող է թարգմանվել նաև այլ կոմպիլյատոր լեզուներով (օրինակ՝ Kotlin, C#, Swift և այլն): Կարելի է փորձել այս մոտեցումը նոր նախագծում և, եթե այն լավ չհամապատասխանի, անցում կատարի հին ձևին:
Իհարկե, կոմպիլյատոր կոնֆիգուրացիան պահանջում է բարձրորակ մշակման գործընթաց: Դրա դիմաց այն խոստանում է ապահովել նույնքան բարձր որակի կայուն կոնֆիգուրացիա:
Այս մոտեցումը կարող է ընդլայնվել տարբեր ձևերով.
- Կարելի է օգտագործել մակրոները՝ կոնֆիգուրացիայի վավերացում կատարելու համար և ձախողվել կոմպիլյացիայի ժամանակ՝ բիզնես-տրամաբանական սահմանափակումների ձախողման դեպքում:
- DSL-ը կարող է իրականացվել՝ կոնֆիգուրացիան դոմենի օգտագործողի համար հարմար ձևով ներկայացնելու համար:
- Ռեսուրսների դինամիկ կառավարում ավտոմատ կազմաձևման կարգավորումներով: Օրինակ, երբ մենք կարգավորում ենք կլաստերի հանգույցների քանակը, մենք կարող ենք ցանկանալ (1) հանգույցները ստանալ մի փոքր փոփոխված կոնֆիգուրացիա; (2) կլաստերի կառավարիչ՝ նոր հանգույցների մասին տեղեկություններ ստանալու համար:
Շնորհակալություն
Ցանկանում եմ շնորհակալություն հայտնել Անդրեյ Սաքսոնովին, Պավել Պոպովին, Անտոն Նեհաևին այս գրառման նախագծի վերաբերյալ ոգեշնչող կարծիք հայտնելու համար, որն օգնեց ինձ ավելի պարզ դարձնել այն:
Source: www.habr.com