Ես կցանկանայի ձեզ պատմել բաշխված համակարգի կոնֆիգուրացիայի հետ աշխատելու մեկ հետաքրքիր մեխանիզմ: Կազմաձևը ներկայացված է ուղղակիորեն կազմված լեզվով (Scala)՝ օգտագործելով անվտանգ տեսակներ: Այս գրառումը ներկայացնում է նման կոնֆիգուրացիայի օրինակ և քննարկում է կազմված կազմաձևման ընդհանուր զարգացման գործընթացում ներդրման տարբեր ասպեկտներ:
(
Ներածություն
Հուսալի բաշխված համակարգ կառուցելը նշանակում է, որ բոլոր հանգույցներն օգտագործում են ճիշտ կոնֆիգուրացիա՝ համաժամանակացված այլ հանգույցների հետ: DevOps տեխնոլոգիաները (terraform, ansible կամ նման բան) սովորաբար օգտագործվում են ավտոմատ կերպով կազմաձևման ֆայլեր ստեղծելու համար (հաճախ հատուկ յուրաքանչյուր հանգույցի համար): Մենք նաև կցանկանայինք վստահ լինել, որ բոլոր հաղորդակցվող հանգույցներն օգտագործում են միանման արձանագրություններ (ներառյալ նույն տարբերակը): Հակառակ դեպքում անհամատեղելիությունը կներկառուցվի մեր բաշխված համակարգում: JVM աշխարհում այս պահանջի հետևանքներից մեկն այն է, որ գրադարանի նույն տարբերակը, որը պարունակում է արձանագրային հաղորդագրություններ, պետք է օգտագործվի ամենուր:
Ինչ վերաբերում է բաշխված համակարգի փորձարկմանը: Իհարկե, մենք ենթադրում ենք, որ բոլոր բաղադրիչներն ունեն միավորի թեստեր, նախքան ինտեգրման թեստավորման անցնելը: (Որպեսզի մենք կարողանանք փորձարկման արդյունքները արտանետել գործարկման ժամանակի վրա, մենք պետք է նաև տրամադրենք գրադարանների նույնական շարք թեստավորման փուլում և գործարկման ժամանակ:)
Ինտեգրման թեստերի հետ աշխատելիս հաճախ ավելի հեշտ է օգտագործել նույն դասընթացը ամենուր բոլոր հանգույցներում: Մեզ մնում է միայն ապահովել, որ գործարկման ժամանակ օգտագործվի նույն դասընթացը: (Չնայած դա լիովին հնարավոր է գործարկել տարբեր հանգույցներ տարբեր դասընթացուղիներով, դա բարդացնում է ընդհանուր կազմաձևը և դժվարություններ տեղակայման և ինտեգրման թեստերի հետ:) Այս գրառման նպատակների համար մենք ենթադրում ենք, որ բոլոր հանգույցները կօգտագործեն նույն դասընթացը:
Կազմաձևը զարգանում է հավելվածի հետ միասին: Մենք օգտագործում ենք տարբերակներ՝ ծրագրի էվոլյուցիայի տարբեր փուլերը բացահայտելու համար: Թվում է, թե տրամաբանական է նաև նույնականացնել կոնֆիգուրացիաների տարբեր տարբերակները: Եվ տեղադրեք կոնֆիգուրացիան ինքնին տարբերակի կառավարման համակարգում: Եթե արտադրության մեջ կա միայն մեկ կոնֆիգուրացիա, ապա մենք կարող ենք պարզապես օգտագործել տարբերակի համարը: Եթե մենք օգտագործում ենք բազմաթիվ արտադրական օրինակներ, ապա մեզ մի քանիսը պետք կգան
կոնֆիգուրացիայի ճյուղեր և տարբերակից բացի լրացուցիչ պիտակ (օրինակ՝ մասնաճյուղի անվանումը): Այս կերպ մենք կարող ենք հստակորեն բացահայտել ճշգրիտ կոնֆիգուրացիան: Յուրաքանչյուր կոնֆիգուրացիայի նույնացուցիչ եզակիորեն համապատասխանում է բաշխված հանգույցների, նավահանգիստների, արտաքին ռեսուրսների և գրադարանի տարբերակների հատուկ համակցությանը: Այս գրառման նպատակների համար մենք կենթադրենք, որ կա միայն մեկ ճյուղ, և մենք կարող ենք նույնականացնել կոնֆիգուրացիան սովորական ձևով՝ օգտագործելով երեք թվեր, որոնք բաժանված են կետով (1.2.3):
Ժամանակակից միջավայրերում կազմաձևման ֆայլերը հազվադեպ են ձեռքով ստեղծվում: Ավելի հաճախ դրանք առաջանում են տեղակայման ժամանակ և այլևս չեն շոշափվում (այնպես որ
Այս գրառման մեջ մենք կուսումնասիրենք կազմված արտեֆակտի ներսում կոնֆիգուրացիա ներկայացնելու գաղափարը:
Կազմված կոնֆիգուրացիա
Այս բաժինը ներկայացնում է ստատիկ կազմված կոնֆիգուրացիայի օրինակ: Իրականացվում են երկու պարզ ծառայություններ՝ էխո ծառայությունը և էխո ծառայության հաճախորդը: Այս երկու ծառայությունների հիման վրա հավաքվում են համակարգի երկու տարբերակ: Մի տարբերակում երկու ծառայություններն էլ գտնվում են նույն հանգույցում, մեկ այլ տարբերակում՝ տարբեր հանգույցներում։
Սովորաբար բաշխված համակարգը պարունակում է մի քանի հանգույց: Դուք կարող եք նույնականացնել հանգույցները՝ օգտագործելով որոշակի տեսակի արժեքներ NodeId
:
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]]
Զտված տեսակներ
Տես գրադարան
HTTP (REST) պրոտոկոլների համար, բացի պորտի համարից, մեզ կարող է անհրաժեշտ լինել նաև ծառայության ուղին.
type UrlPathPrefix = Refined[String, MatchesRegex[W.`"[a-zA-Z_0-9/]*"`.T]]
case class PortWithPrefix[Protocol](portNumber: PortNumber, pathPrefix: UrlPathPrefix)
Ֆանտոմի տեսակները
Կոմպիլյացիայի ժամանակ արձանագրությունը նույնականացնելու համար մենք օգտագործում ենք տիպի պարամետր, որը չի օգտագործվում դասում: Այս որոշումը պայմանավորված է նրանով, որ մենք չենք օգտագործում պրոտոկոլային օրինակ գործարկման ժամանակ, բայց մենք կցանկանայինք, որ կոմպիլյատորը ստուգի արձանագրության համատեղելիությունը: Նշելով արձանագրությունը՝ մենք չենք կարողանա որպես կախվածություն փոխանցել ոչ պատշաճ ծառայություն:
Ընդհանուր արձանագրություններից մեկը REST API-ն է Json սերիալացմամբ.
sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]
որտեղ RequestMessage
- հարցման տեսակը, ResponseMessage
- արձագանքման տեսակը.
Իհարկե, մենք կարող ենք օգտագործել այլ արձանագրությունների նկարագրություններ, որոնք ապահովում են մեր պահանջած նկարագրության ճշգրտությունը:
Այս գրառման նպատակների համար մենք կօգտագործենք արձանագրության պարզեցված տարբերակը.
sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]
Այստեղ հարցումը url-ին կցված տող է, և պատասխանը վերադարձված տող է HTTP պատասխանի մարմնում:
Ծառայության կոնֆիգուրացիան նկարագրվում է ծառայության անունով, նավահանգիստներով և կախվածություններով: Այս տարրերը Scala-ում կարող են ներկայացվել մի քանի ձևով (օրինակ. HList
-ներ, հանրահաշվական տվյալների տեսակներ): Այս գրառման նպատակների համար մենք կօգտագործենք Cake Pattern-ը և կներկայացնենք մոդուլներ՝ օգտագործելով trait
«օվ. (Տորթի ձևանմուշը այս մոտեցման պարտադիր տարր չէ: Դա պարզապես հնարավոր իրականացումներից մեկն է:)
Ծառայությունների միջև կախվածությունը կարող է ներկայացվել որպես նավահանգիստներ վերադարձնող մեթոդներ EndPoint
այլ հանգույցների՝
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 ծառայությունից.
trait EchoClientConfig[A] {
def testMessage: String = "test"
def pollInterval: FiniteDuration
def echoServiceDependency: HttpSimpleGetEndPoint[_, EchoProtocol[A]]
}
Կախվածությունը նույն տեսակի է, ինչ արտահանվող ծառայությունը echoService
. Մասնավորապես, echo հաճախորդում մենք պահանջում ենք նույն արձանագրությունը: Ուստի երկու ծառայություններ միացնելիս կարող ենք վստահ լինել, որ ամեն ինչ ճիշտ կաշխատի։
Ծառայությունների իրականացում
Ծառայությունը սկսելու և դադարեցնելու համար անհրաժեշտ է գործառույթ: (Ծառայությունը դադարեցնելու ունակությունը չափազանց կարևոր է փորձարկման համար:) Կրկին, կան մի քանի տարբերակներ նման գործառույթի իրականացման համար (օրինակ, մենք կարող ենք օգտագործել տիպի դասեր՝ հիմնված կազմաձևման տեսակի վրա): Այս գրառման նպատակների համար մենք կօգտագործենք տորթի նախշը: Մենք կներկայացնենք ծառայությունը՝ օգտագործելով դասը 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
— էֆեկտի տիպի դաս, որը թույլ է տալիս համատեղել առանձին էֆեկտներ (գրեթե մոնադ): Ավելի բարդ ծրագրերում, թվում է, ավելի լավ է օգտագործելMonad
/ConcurrentEffect
.
Օգտագործելով այս ֆունկցիայի ստորագրությունը, մենք կարող ենք իրականացնել մի քանի ծառայություններ: Օրինակ, ծառայություն, որը ոչինչ չի անում.
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
}
Խնդրում ենք նկատի ունենալ, որ մենք նշում ենք կոնֆիգուրացիայի ճշգրիտ տեսակը, որն անհրաժեշտ է այս հանգույցի համար: Եթե մոռանանք նշել որոշակի ծառայության կողմից պահանջվող կոնֆիգուրացիայի տեսակներից մեկը, կառաջանա կոմպիլյացիայի սխալ: Բացի այդ, մենք չենք կարողանա սկսել հանգույց, եթե չտրամադրենք համապատասխան տեսակի օբյեկտ բոլոր անհրաժեշտ տվյալներով:
Հյուրընկալողի անվան լուծում
Հեռավոր հոսթին միանալու համար մեզ անհրաժեշտ է իրական IP հասցե: Հնարավոր է, որ հասցեն հայտնի դառնա ավելի ուշ, քան մնացած կոնֆիգուրացիան: Այսպիսով, մեզ անհրաժեշտ է գործառույթ, որը քարտեզագրում է հանգույցի ID-ն հասցեով.
case class NodeAddress[NodeId](host: Uri.Host)
trait AddressResolver[F[_]] {
def resolve[NodeId](nodeId: NodeId): F[NodeAddress[NodeId]]
}
Այս գործառույթն իրականացնելու մի քանի եղանակ կա.
- Եթե հասցեները մեզ հայտնի են դառնում նախքան տեղակայումը, ապա մենք կարող ենք ստեղծել Scala կոդը
հասցեները և այնուհետև գործարկել build-ը: Սա կկազմի և կգործարկի թեստերը:
Այս դեպքում ֆունկցիան ստատիկորեն հայտնի կլինի և կոդով կարող է ներկայացվել որպես քարտեզագրումMap[NodeId, NodeAddress]
. - Որոշ դեպքերում իրական հասցեն հայտնի է միայն հանգույցի մեկնարկից հետո:
Այս դեպքում մենք կարող ենք իրականացնել «բացահայտման ծառայություն», որն աշխատում է այլ հանգույցներից առաջ, և բոլոր հանգույցները կգրանցվեն այս ծառայության մեջ և կպահանջեն այլ հանգույցների հասցեները: - Եթե մենք կարողանանք փոփոխել
/etc/hosts
, ապա կարող եք օգտագործել նախապես սահմանված հյուրընկալող անունները (ինչպեսmy-project-main-node
иecho-backend
) և ուղղակի կապեք այս անունները
տեղակայման ժամանակ IP հասցեներով:
Այս գրառման մեջ մենք ավելի մանրամասն չենք քննարկի այս դեպքերը: Մեր .... համար
խաղալիքի օրինակում բոլոր հանգույցները կունենան նույն IP հասցեն. 127.0.0.1
.
Հաջորդը, մենք դիտարկում ենք բաշխված համակարգի երկու տարբերակ.
- Բոլոր ծառայությունների տեղադրումը մեկ հանգույցի վրա:
- Եվ echo ծառայության և echo հաճախորդի հոսթինգը տարբեր հանգույցների վրա:
Կոնֆիգուրացիա համար
Մեկ հանգույցի կոնֆիգուրացիա
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
դադարեցնել ծրագիրը. (Ctrl-C-ն նաև ճիշտ է աշխատում և ազատում բոլոր ռեսուրսները:)
Կազմաձևման և իրականացման հատկանիշների նույն շարքը կարող է օգտագործվել համակարգ ստեղծելու համար, որը բաղկացած է
Երկու հանգույցի կոնֆիգուրացիա
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
'om, և հաճախորդի հանգույցն ավարտվում է որոշ ժամանակ անց: Սմ.
Ընդհանուր զարգացման գործընթաց
Տեսնենք, թե ինչպես է այս կոնֆիգուրացիայի մոտեցումն ազդում զարգացման ընդհանուր գործընթացի վրա:
Կազմաձևը կկազմվի մնացած կոդի հետ միասին և կստեղծվի արտեֆակտ (.jar): Կարծես իմաստ ունի կոնֆիգուրացիան դնել առանձին արտեֆակտի մեջ: Դա պայմանավորված է նրանով, որ մենք կարող ենք ունենալ մի քանի կոնֆիգուրացիաներ՝ հիմնված նույն կոդի վրա: Կրկին, հնարավոր է ստեղծել տարբեր կոնֆիգուրացիայի ճյուղերին համապատասխան արտեֆակտներ: Գրադարանների որոշակի տարբերակներից կախվածությունը պահպանվում է կազմաձևման հետ մեկտեղ, և այս տարբերակները պահվում են ընդմիշտ, երբ մենք որոշենք տեղակայել կազմաձևման այդ տարբերակը:
Ցանկացած կոնֆիգուրացիայի փոփոխություն վերածվում է կոդի փոփոխության: Եվ հետևաբար, յուրաքանչյուրը
փոփոխությունը ծածկվելու է որակի ապահովման նորմալ գործընթացով.
Տոմս սխալների հետագծում -> PR -> վերանայում -> միաձուլվել համապատասխան մասնաճյուղերի հետ ->
ինտեգրում -> տեղակայում
Կազմված կոնֆիգուրացիայի իրականացման հիմնական հետևանքները հետևյալն են.
-
Կազմաձևը հետևողական կլինի բաշխված համակարգի բոլոր հանգույցներում: Շնորհիվ այն բանի, որ բոլոր հանգույցները ստանում են նույն կոնֆիգուրացիան մեկ աղբյուրից:
-
Խնդրահարույց է կոնֆիգուրացիան փոխել հանգույցներից միայն մեկում։ Հետևաբար, «կոնֆիգուրացիայի շեղումը» քիչ հավանական է:
-
Կազմաձևում փոքր փոփոխություններ կատարելն ավելի դժվար է դառնում:
-
Կազմաձևման փոփոխությունների մեծ մասը տեղի կունենա որպես ընդհանուր զարգացման գործընթացի մաս և ենթակա կլինի վերանայման:
Արդյո՞ք ինձ անհրաժեշտ է առանձին պահեստ՝ արտադրության կոնֆիգուրացիան պահելու համար: Այս կոնֆիգուրացիան կարող է պարունակել գաղտնաբառեր և այլ զգայուն տեղեկություններ, որոնց մուտքը մենք կցանկանայինք սահմանափակել: Ելնելով դրանից՝ թվում է, թե իմաստ ունի պահպանել վերջնական կոնֆիգուրացիան առանձին պահեստում: Դուք կարող եք կոնֆիգուրացիան բաժանել երկու մասի. մեկը պարունակում է հանրությանը հասանելի կազմաձևման կարգավորումներ, իսկ մյուսը պարունակում է սահմանափակ կարգավորումներ: Սա ծրագրավորողների մեծամասնությանը թույլ կտա մուտք գործել ընդհանուր կարգավորումներ: Այս տարանջատումը հեշտ է հասնել՝ օգտագործելով լռելյայն արժեքներ պարունակող միջանկյալ հատկանիշներ:
Հնարավոր տատանումներ
Փորձենք համեմատել կազմված կոնֆիգուրացիան որոշ ընդհանուր այլընտրանքների հետ.
- Տեքստային ֆայլ թիրախային մեքենայի վրա:
- Կենտրոնացված բանալի-արժեքի խանութ (
etcd
/zookeeper
). - Գործընթացի բաղադրիչներ, որոնք կարող են վերակազմավորվել/վերագործարկվել առանց գործընթացը վերագործարկելու:
- Կազմաձևերի պահպանում արտեֆակտի և տարբերակի վերահսկողությունից դուրս:
Տեքստային ֆայլերը զգալի ճկունություն են ապահովում փոքր փոփոխությունների առումով: Համակարգի ադմինիստրատորը կարող է մուտք գործել հեռավոր հանգույց, փոփոխություններ կատարել համապատասխան ֆայլերում և վերագործարկել ծառայությունը: Խոշոր համակարգերի համար, սակայն, նման ճկունությունը չի կարող ցանկալի լինել: Կատարված փոփոխությունները այլ համակարգերում հետք չեն թողնում։ Ոչ ոք չի վերանայում փոփոխությունները։ Դժվար է որոշել, թե կոնկրետ ով և ինչ պատճառով է կատարել փոփոխությունները։ Փոփոխությունները չեն փորձարկվում: Եթե համակարգը բաշխված է, ապա ադմինիստրատորը կարող է մոռանալ կատարել համապատասխան փոփոխություն այլ հանգույցների վրա։
(Հարկ է նաև նշել, որ կոմպիլացված կոնֆիգուրացիայի օգտագործումը չի փակում ապագայում տեքստային ֆայլերի օգտագործման հնարավորությունը: Բավական կլինի ավելացնել վերլուծիչ և վավերացնող, որը արտադրում է նույն տիպը, ինչ ելքը: Config
, և կարող եք օգտագործել տեքստային ֆայլեր: Անմիջապես հետևում է, որ կազմված կազմաձևով համակարգի բարդությունը փոքր-ինչ ավելի քիչ է, քան տեքստային ֆայլեր օգտագործող համակարգի բարդությունը, քանի որ տեքստային ֆայլերը պահանջում են լրացուցիչ կոդ:)
Կենտրոնացված բանալի-արժեքի խանութը լավ մեխանիզմ է բաշխված հավելվածի մետա պարամետրերը բաշխելու համար: Մենք պետք է որոշենք, թե որոնք են կազմաձևման պարամետրերը և որոնք են պարզապես տվյալները: Եկեք գործառույթ ունենանք C => A => B
, և պարամետրերը C
հազվադեպ է փոխվում, և տվյալները A
- հաճախ. Այս դեպքում կարելի է ասել, որ C
- կազմաձևման պարամետրեր և A
- տվյալներ. Ըստ երևույթին, կազմաձևման պարամետրերը տարբերվում են տվյալներից նրանով, որ դրանք սովորաբար ավելի քիչ են փոխվում, քան տվյալները: Բացի այդ, տվյալները սովորաբար ստացվում են մեկ աղբյուրից (օգտագործողի կողմից), իսկ կազմաձևման պարամետրերը մեկ այլ աղբյուրից (համակարգի ադմինիստրատորից):
Եթե հազվադեպ փոփոխվող պարամետրերը պետք է թարմացվեն առանց ծրագիրը վերագործարկելու, ապա դա հաճախ կարող է հանգեցնել ծրագրի բարդացման, քանի որ մեզ անհրաժեշտ կլինի ինչ-որ կերպ մատուցել պարամետրերը, պահել, վերլուծել և ստուգել և մշակել սխալ արժեքներ: Հետևաբար, ծրագրի բարդությունը նվազեցնելու տեսակետից իմաստ ունի նվազեցնել այն պարամետրերի քանակը, որոնք կարող են փոխվել ծրագրի շահագործման ընթացքում (կամ ընդհանրապես չաջակցել նման պարամետրերին):
Այս գրառման նպատակների համար մենք կտարբերակենք ստատիկ և դինամիկ պարամետրերը: Եթե ծառայության տրամաբանությունը պահանջում է ծրագրի գործարկման ընթացքում պարամետրերի փոփոխություն, ապա այդպիսի պարամետրերը կանվանենք դինամիկ։ Հակառակ դեպքում ընտրանքները ստատիկ են և կարող են կազմաձևվել՝ օգտագործելով կազմված կոնֆիգուրացիան: Դինամիկ վերակազմավորման համար մեզ կարող է անհրաժեշտ լինել ծրագրի մասերը նոր պարամետրերով վերագործարկելու մեխանիզմ, որը նման է օպերացիոն համակարգի գործընթացների վերագործարկմանը: (Մեր կարծիքով, նպատակահարմար է խուսափել իրական ժամանակի վերակազմավորումից, քանի որ դա մեծացնում է համակարգի բարդությունը: Հնարավորության դեպքում ավելի լավ է օգտագործել ստանդարտ ՕՀ հնարավորությունները գործընթացները վերագործարկելու համար):
Ստատիկ կոնֆիգուրացիայի օգտագործման կարևոր ասպեկտներից մեկը, որը մարդկանց ստիպում է դիտարկել դինամիկ վերակազմավորումը, այն ժամանակն է, որը տևում է, որպեսզի համակարգը վերագործարկվի կազմաձևման թարմացումից հետո (անդադար): Փաստորեն, եթե մեզ անհրաժեշտ լինի փոփոխություններ կատարել ստատիկ կազմաձևում, մենք ստիպված կլինենք վերագործարկել համակարգը, որպեսզի նոր արժեքներն ուժի մեջ մտնեն: Դադարեցման խնդիրը տարբեր համակարգերի համար տարբերվում է ծանրությունից: Որոշ դեպքերում, դուք կարող եք պլանավորել վերաբեռնում այն ժամանակ, երբ բեռը նվազագույն է: Եթե Ձեզ անհրաժեշտ է շարունակական ծառայություն մատուցել, կարող եք իրականացնել
Այժմ դիտարկենք կոնֆիգուրացիան արտեֆակտի ներսում կամ դրսում պահելու հարցը: Եթե մենք պահպանում ենք կոնֆիգուրացիան արտեֆակտի ներսում, ապա գոնե մենք հնարավորություն ունեինք ստուգել կոնֆիգուրացիայի ճիշտությունը արտեֆակտի հավաքման ժամանակ: Եթե կոնֆիգուրացիան դուրս է վերահսկվող արտեֆակտից, դժվար է հետևել, թե ով և ինչու է փոփոխություններ կատարել այս ֆայլում: Որքանո՞վ է դա կարևոր: Մեր կարծիքով, շատ արտադրական համակարգերի համար կարևոր է ունենալ կայուն և որակյալ կոնֆիգուրացիա:
Արտեֆակտի տարբերակը թույլ է տալիս որոշել, թե երբ է այն ստեղծվել, ինչ արժեքներ է պարունակում, ինչ գործառույթներ են միացված/անջատված, և ով է պատասխանատու կոնֆիգուրացիայի ցանկացած փոփոխության համար: Իհարկե, կոնֆիգուրացիան արտեֆակտի ներսում պահելը որոշակի ջանք է պահանջում, այնպես որ դուք պետք է տեղեկացված որոշում կայացնեք:
Ընդդիմության եւ հակառակը
Ես կցանկանայի կանգ առնել առաջարկվող տեխնոլոգիայի դրական և բացասական կողմերի վրա:
Առավելությունները
Ստորև բերված է կազմված բաշխված համակարգի կազմաձևման հիմնական հատկանիշների ցանկը.
- Ստատիկ կոնֆիգուրացիայի ստուգում: Թույլ է տալիս վստահ լինել դրանում
կոնֆիգուրացիան ճիշտ է: - Հարուստ կազմաձևման լեզու: Սովորաբար, այլ կազմաձևման մեթոդները սահմանափակվում են առավելագույնը լարային փոփոխականի փոխարինմամբ: Scala-ն օգտագործելիս հասանելի են լեզվական գործառույթների լայն շրջանակ՝ ձեր կազմաձևումը բարելավելու համար: Օրինակ, մենք կարող ենք օգտագործել
լռելյայն արժեքների գծերը, օգտագործելով օբյեկտները պարամետրերը խմբավորելու համար, մենք կարող ենք հղում անել միայն մեկ անգամ (DRY) հայտարարված val-ներին կցվող տիրույթում: Դուք կարող եք ակնարկել ցանկացած դաս անմիջապես կոնֆիգուրացիայի ներսում (Seq
,Map
, մաքսային դասեր)։ - DSL. Scala-ն ունի մի շարք լեզվական առանձնահատկություններ, որոնք հեշտացնում են DSL-ի ստեղծումը: Հնարավոր է օգտվել այս հնարավորություններից և կիրառել կոնֆիգուրացիայի լեզու, որն ավելի հարմար է օգտատերերի թիրախային խմբի համար, որպեսզի կոնֆիգուրացիան գոնե ընթեռնելի լինի տիրույթի փորձագետների կողմից: Մասնագետները կարող են, օրինակ, մասնակցել կոնֆիգուրացիայի վերանայման գործընթացին:
- Ամբողջականություն և սինխրոնիա հանգույցների միջև: Ամբողջ բաշխված համակարգի կոնֆիգուրացիան մեկ կետում պահելու առավելություններից մեկն այն է, որ բոլոր արժեքները հայտարարվում են ուղիղ մեկ անգամ, այնուհետև նորից օգտագործվում են այնտեղ, որտեղ անհրաժեշտ է: Ֆանտոմային տեսակների օգտագործումը նավահանգիստները հայտարարելու համար ապահովում է, որ հանգույցները օգտագործում են համատեղելի արձանագրություններ բոլոր ճիշտ համակարգի կոնֆիգուրացիաներում: Հանգույցների միջև հստակ պարտադիր կախվածություն ունենալը ապահովում է, որ բոլոր ծառայությունները միացված են:
- Բարձր որակի փոփոխություններ։ Կազմաձևում փոփոխություններ կատարելը, օգտագործելով մշակման ընդհանուր գործընթաց, հնարավորություն է տալիս հասնել նաև կազմաձևման բարձր որակի չափանիշներին:
- Միաժամանակյա կոնֆիգուրացիայի թարմացում: Համակարգի ավտոմատ տեղակայումը կազմաձևման փոփոխություններից հետո ապահովում է, որ բոլոր հանգույցները թարմացվում են:
- Հավելվածի պարզեցում. Հավելվածը վերլուծության, կազմաձևման ստուգման կամ սխալ արժեքների մշակման կարիք չունի: Սա նվազեցնում է հավելվածի բարդությունը: (Մեր օրինակում նկատված կազմաձևման որոշ բարդություններ կազմված կոնֆիգուրացիայի հատկանիշ չէ, այլ միայն գիտակցված որոշում, որը պայմանավորված է ավելի մեծ տիպի անվտանգություն ապահովելու ցանկությամբ:) Բավականին հեշտ է վերադառնալ սովորական կազմաձևին. պարզապես կատարեք բացակայողը: մասեր. Հետևաբար, դուք կարող եք, օրինակ, սկսել կազմված կոնֆիգուրացիայից՝ հետաձգելով ավելորդ մասերի իրականացումը մինչև այն պահը, երբ դա իսկապես անհրաժեշտ է:
- Ստուգված կոնֆիգուրացիա: Քանի որ կազմաձևման փոփոխությունները հետևում են ցանկացած այլ փոփոխությունների սովորական ճակատագրին, ստացված արդյունքը եզակի տարբերակով արտեֆակտ է: Սա մեզ թույլ է տալիս, օրինակ, անհրաժեշտության դեպքում վերադառնալ կոնֆիգուրացիայի նախորդ տարբերակին: Մենք նույնիսկ կարող ենք օգտագործել մեկ տարի առաջվա կոնֆիգուրացիան, և համակարգը կաշխատի նույն կերպ: Կայուն կոնֆիգուրացիան բարելավում է բաշխված համակարգի կանխատեսելիությունն ու հուսալիությունը: Քանի որ կոնֆիգուրացիան ամրագրված է կազմման փուլում, բավականին դժվար է այն կեղծել արտադրության մեջ:
- Մոդուլյարություն. Առաջարկվող շրջանակը մոդուլային է, և մոդուլները կարող են համակցվել տարբեր ձևերով՝ տարբեր համակարգեր ստեղծելու համար: Մասնավորապես, դուք կարող եք կարգավորել համակարգը այնպես, որ գործարկվի մեկ հանգույցի վրա մեկ մարմնավորման դեպքում, և մի քանի հանգույցների վրա՝ մեկ այլ մարմնում: Դուք կարող եք ստեղծել մի քանի կոնֆիգուրացիաներ համակարգի արտադրության օրինակների համար:
- Փորձարկում. Անհատական ծառայությունները կեղծ օբյեկտներով փոխարինելով՝ կարող եք ձեռք բերել համակարգի մի քանի տարբերակներ, որոնք հարմար են թեստավորման համար։
- Ինտեգրման փորձարկում. Ամբողջ բաշխված համակարգի համար մեկ կոնֆիգուրացիա ունենալը հնարավորություն է տալիս բոլոր բաղադրիչները գործարկել վերահսկվող միջավայրում՝ որպես ինտեգրման փորձարկման մաս: Հեշտ է ընդօրինակել, օրինակ, մի իրավիճակ, երբ որոշ հանգույցներ հասանելի են դառնում:
Թերություններ և սահմանափակումներ
Կազմված կոնֆիգուրացիան տարբերվում է կազմաձևման այլ մոտեցումներից և կարող է հարմար չլինել որոշ հավելվածների համար: Ստորև բերված են որոշ թերություններ.
- Ստատիկ կոնֆիգուրացիա: Երբեմն անհրաժեշտ է արագ շտկել կոնֆիգուրացիան արտադրության մեջ՝ շրջանցելով բոլոր պաշտպանիչ մեխանիզմները: Այս մոտեցմամբ դա կարող է ավելի դժվար լինել: Առնվազն, դեռևս կպահանջվի հավաքում և ավտոմատ տեղակայում: Սա և՛ մոտեցման օգտակար հատկանիշն է, և՛ որոշ դեպքերում թերություն:
- Կազմաձևման ձևավորում: Այն դեպքում, երբ կազմաձևման ֆայլը ստեղծվում է ավտոմատ գործիքի միջոցով, լրացուցիչ ջանքեր կարող են պահանջվել կառուցման սցենարը ինտեգրելու համար:
- Գործիքներ. Ներկայումս կոմունալ ծառայություններն ու տեխնիկան, որոնք նախատեսված են կոնֆիգուրացիայի հետ աշխատելու համար, հիմնված են տեքստային ֆայլերի վրա: Ոչ բոլոր նման կոմունալ ծառայությունները/տեխնիկան հասանելի կլինի կազմված կազմաձևում:
- Պահանջվում է վերաբերմունքի փոփոխություն. Մշակողները և DevOps-ը սովոր են տեքստային ֆայլերին: Կազմաձևման գաղափարը կարող է որոշ չափով անսպասելի և անսովոր լինել և մերժման պատճառ դառնալ:
- Պահանջվում է բարձրորակ զարգացման գործընթաց: Կազմված կոնֆիգուրացիան հարմարավետորեն օգտագործելու համար անհրաժեշտ է հավելվածի կառուցման և տեղակայման գործընթացի ամբողջական ավտոմատացում (CI/CD): Հակառակ դեպքում դա բավականին անհարմար կլինի։
Եկեք խոսենք նաև դիտարկված օրինակի մի շարք սահմանափակումների վրա, որոնք կապված չեն կազմված կազմաձևման գաղափարի հետ.
- Եթե մենք տրամադրենք անհարկի կոնֆիգուրացիայի տեղեկատվություն, որը չի օգտագործվում հանգույցի կողմից, ապա կոմպիլյատորը չի օգնի մեզ հայտնաբերել բացակայող իրականացումը: Այս խնդիրը կարող է լուծվել՝ հրաժարվելով Cake Pattern-ից և օգտագործելով ավելի կոշտ տեսակներ, օրինակ.
HList
կամ հանրահաշվական տվյալների տեսակները (case classes)՝ կոնֆիգուրացիան ներկայացնելու համար: - Կազմաձևման ֆայլում կան տողեր, որոնք կապված չեն հենց կազմաձևման հետ.
package
,import
,օբյեկտների հայտարարագրեր;override def
-ը այն պարամետրերի համար, որոնք ունեն լռելյայն արժեքներ): Սա կարելի է մասամբ խուսափել, եթե դուք ներդրում եք ձեր սեփական DSL-ը: Բացի այդ, այլ տեսակի կոնֆիգուրացիաներ (օրինակ՝ XML) նույնպես որոշակի սահմանափակումներ են դնում ֆայլի կառուցվածքի վրա։ - Այս գրառման նպատակների համար մենք չենք դիտարկում նմանատիպ հանգույցների կլաստերի դինամիկ վերակազմավորումը:
Ամփոփում
Այս գրառման մեջ մենք ուսումնասիրեցինք սկզբնական կոդով կազմաձևումը ներկայացնելու գաղափարը՝ օգտագործելով Scala տիպի համակարգի առաջադեմ հնարավորությունները: Այս մոտեցումը կարող է օգտագործվել տարբեր հավելվածներում՝ որպես xml կամ տեքստային ֆայլերի վրա հիմնված ավանդական կազմաձևման մեթոդների փոխարինում: Չնայած մեր օրինակն իրականացվում է Scala-ում, նույն գաղափարները կարող են փոխանցվել այլ կազմված լեզուների (օրինակ՝ Kotlin, C#, Swift, ...): Դուք կարող եք փորձել այս մոտեցումը հետևյալ նախագծերից մեկում և, եթե այն չի աշխատում, անցեք տեքստային ֆայլին՝ ավելացնելով բաց թողնված մասերը։
Բնականաբար, կազմված կոնֆիգուրացիան պահանջում է բարձրորակ մշակման գործընթաց: Դրա դիմաց ապահովվում է կոնֆիգուրացիաների բարձր որակ և հուսալիություն:
Դիտարկվող մոտեցումը կարող է ընդլայնվել.
- Կազմելու ժամանակի ստուգումներ կատարելու համար կարող եք օգտագործել մակրոները:
- Դուք կարող եք իրականացնել DSL՝ կոնֆիգուրացիան վերջնական օգտագործողների համար հասանելի ձևով ներկայացնելու համար:
- Դուք կարող եք իրականացնել ռեսուրսների դինամիկ կառավարում ավտոմատ կազմաձևման ճշգրտմամբ: Օրինակ, կլաստերի մեջ հանգույցների քանակի փոփոխությունը պահանջում է, որ (1) յուրաքանչյուր հանգույց ստանա մի փոքր այլ կոնֆիգուրացիա; (2) կլաստերի կառավարիչը տեղեկատվություն է ստացել նոր հանգույցների մասին:
Շնորհակալագրեր
Ցանկանում եմ շնորհակալություն հայտնել Անդրեյ Սաքսոնովին, Պավել Պոպովին և Անտոն Նեխաևին հոդվածի նախագծի վերաբերյալ կառուցողական քննադատության համար։
Source: www.habr.com